Test/node_modules/@excalidraw/excalidraw/dist/dev/index.js
2026-04-09 22:54:00 +07:00

33142 lines
1.1 MiB

import {
APP_NAME,
ARROW_TYPE,
AbortError,
AlignBottomIcon,
AlignLeftIcon,
AlignRightIcon,
AlignTopIcon,
ArrowRightIcon,
ArrowheadArrowIcon,
ArrowheadBarIcon,
ArrowheadCircleIcon,
ArrowheadCircleOutlineIcon,
ArrowheadCrowfootIcon,
ArrowheadCrowfootOneIcon,
ArrowheadCrowfootOneOrManyIcon,
ArrowheadDiamondIcon,
ArrowheadDiamondOutlineIcon,
ArrowheadNoneIcon,
ArrowheadTriangleIcon,
ArrowheadTriangleOutlineIcon,
BINDING_HIGHLIGHT_OFFSET,
BINDING_HIGHLIGHT_THICKNESS,
BOUND_TEXT_PADDING,
BringForwardIcon,
BringToFrontIcon,
CANVAS_SEARCH_TAB,
CLASSES,
CODES,
COLORS_PER_ROW,
COLOR_CHARCOAL_BLACK,
COLOR_PALETTE,
COLOR_VOICE_CALL,
COLOR_WHITE,
CURSOR_TYPE,
CaptureUpdateAction,
CenterHorizontallyIcon,
CenterVerticallyIcon,
CloseIcon,
DEFAULT_CANVAS_BACKGROUND_PICKS,
DEFAULT_COLLISION_THRESHOLD,
DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX,
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
DEFAULT_ELEMENT_BACKGROUND_PICKS,
DEFAULT_ELEMENT_STROKE_COLOR_INDEX,
DEFAULT_ELEMENT_STROKE_COLOR_PALETTE,
DEFAULT_ELEMENT_STROKE_PICKS,
DEFAULT_EXPORT_PADDING,
DEFAULT_FILENAME,
DEFAULT_FONT_FAMILY,
DEFAULT_FONT_SIZE,
DEFAULT_GRID_SIZE,
DEFAULT_LASER_COLOR,
DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT,
DEFAULT_REDUCED_GLOBAL_ALPHA,
DEFAULT_SIDEBAR,
DEFAULT_TEXT_ALIGN,
DEFAULT_TRANSFORM_HANDLE_SPACING,
DEFAULT_UI_OPTIONS,
DEFAULT_VERTICAL_ALIGN,
DRAGGING_THRESHOLD,
DeviceDesktopIcon,
DiscordIcon,
DistributeHorizontallyIcon,
DistributeVerticallyIcon,
DotsIcon,
DuplicateIcon,
EDITOR_LS_KEYS,
ELEMENT_SHIFT_TRANSLATE_AMOUNT,
ELEMENT_TRANSLATE_AMOUNT,
ENV,
EXPORT_DATA_TYPES,
EXPORT_IMAGE_TYPES,
EXPORT_SCALES,
EXPORT_SOURCE,
EdgeRoundIcon,
EdgeSharpIcon,
EmbedIcon,
Emitter,
ExportIcon,
ExportImageIcon,
ExternalLinkIcon,
FONT_FAMILY,
FRAME_STYLE,
FillCrossHatchIcon,
FillHachureIcon,
FillSolidIcon,
FillZigZagIcon,
FontFamilyCodeIcon,
FontFamilyNormalIcon,
FontSizeExtraLargeIcon,
FontSizeLargeIcon,
FontSizeMediumIcon,
FontSizeSmallIcon,
Fonts,
FreedrawIcon,
GithubIcon,
GroupIcon,
HEADING_DOWN,
HEADING_LEFT,
HEADING_RIGHT,
HEADING_UP,
HYPERLINK_TOOLTIP_DELAY,
HamburgerMenuIcon,
HelpIcon,
HelpIconThin,
IMAGE_MIME_TYPES,
IMAGE_RENDER_TIMEOUT,
ImageSceneDataError,
ImageURLToFile,
KEYS,
LIBRARY_DISABLED_TYPES,
LIBRARY_SIDEBAR_TAB,
LINE_CONFIRM_THRESHOLD,
LibraryIcon,
LinearElementEditor,
LinkIcon,
LoadIcon,
LockedIcon,
MAX_ALLOWED_FILE_BYTES,
MAX_CUSTOM_COLORS_USED_IN_CANVAS,
MAX_ZOOM,
MIME_TYPES,
MINIMAL_CROP_SIZE,
MIN_WIDTH_OR_HEIGHT,
MIN_ZOOM,
MQ_MAX_HEIGHT_LANDSCAPE,
MQ_MAX_WIDTH_LANDSCAPE,
MQ_MAX_WIDTH_PORTRAIT,
MQ_RIGHT_SIDEBAR_MIN_WIDTH,
MagicIcon,
MoonIcon,
POINTER_BUTTON,
POINTER_EVENTS,
PenModeIcon,
PinIcon,
PlusIcon,
ROUNDNESS,
RedoIcon,
SCROLL_TIMEOUT,
SHAPES,
STATS_PANELS,
STROKE_WIDTH,
SVGStringToFile,
SVG_NS,
Scene_default,
SendBackwardIcon,
SendToBackIcon,
ShapeCache,
SloppinessArchitectIcon,
SloppinessArtistIcon,
SloppinessCartoonistIcon,
SnapCache,
Store,
StrokeStyleDashedIcon,
StrokeStyleDottedIcon,
StrokeWidthBaseIcon,
StrokeWidthBoldIcon,
StrokeWidthExtraBoldIcon,
SunIcon,
TAP_TWICE_TIMEOUT,
TEXT_ALIGN,
TEXT_TO_CENTER_SNAP_THRESHOLD,
THEME,
THEME_FILTER,
TOOL_TYPE,
TOUCH_CTX_MENU_TIMEOUT,
TextAlignBottomIcon,
TextAlignCenterIcon,
TextAlignLeftIcon,
TextAlignMiddleIcon,
TextAlignRightIcon,
TextAlignTopIcon,
TextIcon,
TrashIcon,
URL_HASH_KEYS,
URL_QUERY_KEYS,
UndoIcon,
UngroupIcon,
UnlockedIcon,
UserIdleState,
VERSIONS,
VERTICAL_ALIGN,
WelcomeScreenHelpArrow,
WelcomeScreenMenuArrow,
WelcomeScreenTopToolbarArrow,
XBrandIcon,
YOUTUBE_STATES,
ZOOM_STEP,
ZoomInIcon,
ZoomOutIcon,
ZoomResetIcon,
aabbForElement,
abacusIcon,
actions,
addElementsToFrame,
addEventListener,
addToGroup,
alertTriangleIcon,
angleIcon,
arrayToList,
arrayToMap,
arrayToMapWithIndex,
assertNever,
bindElementsToFramesAfterDuplication,
bindLinearElement,
bindOrUnbindLinearElement,
bindOrUnbindLinearElements,
bindPointToSnapToElementOutline,
bindTextToShapeAfterDuplication,
boltIcon,
bootstrapCanvas,
brainIcon,
brainIconThin,
bucketFillIcon,
bumpVersion,
calculateFixedPointForElbowArrowBinding,
calculateScrollCenter,
canApplyRoundnessTypeToElement,
canChangeRoundness,
canCreateLinkFromElements,
canHaveArrowheads,
canvasToBlob,
capitalizeString,
castArray,
centerScrollOn,
checkIcon,
chunk,
clamp,
clockIcon,
cloneJSON,
coffeeIcon,
collapseDownIcon,
collapseUpIcon,
compareHeading,
composeEventHandlers,
computeBoundTextPosition,
computeContainerDimensionForBoundText,
copyBlobToClipboardAsPng,
copyIcon,
copyTextToSystemClipboard,
copyToClipboard,
createPasteEvent,
createSrcDoc,
cropElement,
cropIcon,
cutIcon,
dataURLToFile,
dataURLToString,
debounce,
deepCopyElement,
defaultGetElementLinkFromSelection,
degreesToRadians,
distance,
done,
downloadIcon,
dragNewElement,
dragSelectedElements,
duplicateElement,
duplicateElements,
easeOut,
easeToValuesRAF,
editGroupForSelectedElement,
elbowArrowIcon,
elementLinkIcon,
elementOverlapsWithFrame,
elementPartiallyOverlapsWithOrContainsBBox,
elementsAreInFrameBounds,
elementsAreInSameGroup,
elementsOverlappingBBox,
embeddableURLValidator,
excludeElementsInFramesFromSelection,
exportToBlob,
exportToCanvas,
exportToCanvas2,
exportToClipboard,
exportToFileIcon,
exportToSvg,
exportToSvg2,
extraToolsIcon,
eyeClosedIcon,
eyeDropperIcon,
eyeIcon,
fileOpen,
fileSave,
fillCircle,
filterElementsEligibleAsFrameChildren,
findIndex,
findLastIndex,
findShapeByKey,
fixBindingsAfterDeletion,
fixBindingsAfterDuplication,
flipHorizontal,
flipVertical,
focusNearestParent,
fontSizeIcon,
frameAndChildrenSelectedTogether,
frameToolIcon,
fullscreenIcon,
generateIdFromFile,
getApproxMinLineHeight,
getApproxMinLineWidth,
getBoundTextElement,
getBoundTextElementId,
getBoundTextMaxHeight,
getBoundTextMaxWidth,
getBoundTextShape,
getCommonAttributeOfSelectedElements,
getCommonBoundingBox,
getCommonBounds,
getContainerCenter,
getContainerElement,
getContainingFrame,
getCornerRadius,
getCursorForResizingElement,
getDataURL,
getDataURL_sync,
getDateTime,
getDefaultAppState,
getDefaultRoundnessTypeForElement,
getDragOffsetXY,
getElementAbsoluteCoords,
getElementShape,
getElementWithTransformHandleType,
getElementsInGroup,
getElementsInNewFrame,
getElementsInResizingFrame,
getElementsOverlappingFrame,
getElementsWithinSelection,
getEmbedLink,
getExportSize,
getFileFromEvent,
getFileHandleType,
getFlipAdjustedCropPosition,
getFontFamilyString,
getFontString,
getFrame,
getFrameChildren,
getFrameLikeElements,
getFrameLikeTitle,
getFreeDrawSvgPath,
getGlobalCSSVariable,
getGridPoint,
getHoveredElementForBinding,
getInitializedImageElements,
getLineHeight,
getLineHeightInPx,
getLinkHandleFromCoords,
getLinkIdAndTypeFromSelection,
getLockedLinearCursorAlignSize,
getMaximumGroups,
getMinTextElementWidth,
getNearestScrollableContainer,
getNonDeletedElements,
getNormalizedCanvasDimensions,
getNormalizedDimensions,
getNormalizedGridStep,
getNormalizedZoom,
getOmitSidesForDevice,
getOriginalContainerHeightFromCache,
getReferenceSnapPoints,
getRenderOpacity,
getResizeArrowDirection,
getResizeOffsetXY,
getRootElements,
getSceneVersion,
getSelectedElements,
getSelectedGroupForElement,
getSelectedGroupIdForElement,
getSelectedGroupIds,
getSelectionBoxShape,
getShortcutKey,
getSizeFromPoints,
getSnapLinesAtPointer,
getSuggestedBindingsForArrows,
getSvgPathFromStroke,
getTargetElements,
getTextElementAngle,
getTextFromElements,
getTextWidth,
getTransformHandleTypeFromCoords,
getTransformHandles,
getTransformHandlesFromCoords,
getUncroppedWidthAndHeight,
getVisibleGaps,
getVisibleSceneBounds,
gridIcon,
groupByFrameLikes,
handIcon,
handleBindTextResize,
hasBackground,
hasBoundTextElement,
hasStrokeColor,
hasStrokeStyle,
hasStrokeWidth,
hashElementsVersion,
hashString,
headingForPointFromElement,
helpIcon,
hitElementBoundText,
hitElementBoundingBox,
hitElementBoundingBoxOnly,
hitElementItself,
invariant,
isActiveToolNonLinearSnappable,
isArrowElement,
isArrowKey,
isBindableElement,
isBindingElement,
isBindingElementType,
isBindingEnabled,
isBoundToContainer,
isBrave,
isCursorInFrame,
isDarwin,
isDevEnv,
isElbowArrow,
isElementCompletelyInViewport,
isElementInFrame,
isElementInGroup,
isElementInViewport,
isElementInsideBBox,
isElementLink,
isEmbeddableElement,
isEraserActive,
isExcalidrawElement,
isFiniteNumber,
isFirefox,
isFlowchartNodeElement,
isFrameElement,
isFrameLikeElement,
isGridModeEnabled,
isHandToolActive,
isIOS,
isIframeElement,
isIframeLikeElement,
isImageElement,
isImageFileHandle,
isImageFileHandleType,
isInGroup,
isInitializedImageElement,
isInputLike,
isInteractive,
isInvisiblySmallElement,
isLinearElement,
isLinearElementSimpleAndAlreadyBound,
isLinearElementType,
isLocalLink,
isMagicFrameElement,
isMeasureTextSupported,
isMemberOf,
isNonDeletedElement,
isPathALoop,
isPointHittingLink,
isPointHittingLinkIcon,
isPointInShape,
isPromiseLike,
isSafari,
isSelectedViaGroup,
isShallowEqual,
isSnappingEnabled,
isSomeElementSelected,
isSupportedImageFile,
isTestEnv,
isTextBindableContainer,
isTextElement,
isToolIcon,
isTransparent,
isUsingAdaptiveRadius,
isValidTextContainer,
isWindows,
isWritableElement,
laserPointerToolIcon,
lineEditorIcon,
loadFromBlob,
loadFromJSON,
loadHTMLImageElement,
loadLibraryFromBlob,
loadSceneOrLibraryFromBlob,
magnetIcon,
makeNextSelectedElementIds,
matchKey,
maxBindingGap,
maybeBindLinearElement,
maybeParseEmbedSrc,
measureText,
memoize,
mermaidLogoIcon,
microphoneIcon,
microphoneMutedIcon,
mutateElement,
muteFSAbortError,
nativeFileSystemSupported,
newArrowElement,
newElement,
newElementWith,
newEmbeddableElement,
newFrameElement,
newFreeDrawElement,
newIframeElement,
newImageElement,
newLinearElement,
newMagicFrameElement,
newTextElement,
normalizeEOL,
normalizeFile,
normalizeLink,
normalizeSVG,
normalizeText,
orderByFractionalIndex,
originalContainerCache,
paintIcon,
palette,
parseClipboard,
parseElementLinkFromURL,
parseLibraryJSON,
pngIcon,
pointDistance,
pointFrom,
pointRotateRads,
preventUnload,
probablySupportsClipboardBlob,
probablySupportsClipboardWriteText,
promiseTry,
publishIcon,
queryFocusableElements,
questionCircle,
radiansToDegrees,
randomId,
randomInteger,
readSystemClipboard,
redrawTextBoundingBox,
refreshTextDimensions,
register,
removeAllElementsFromFrame,
removeElementsFromFrame,
removeFromSelectedGroups,
renderElement,
renderSelectionElement,
renderSpreadsheet,
renderStaticScene,
renderStaticSceneThrottled,
replaceAllElementsInFrame,
rescalePointsInElement,
resetCursor,
resetOriginalContainerCache,
resizeImageFile,
resizeMultipleElements,
resizeSingleElement,
resolvablePromise,
restore,
restoreAppState,
restoreElements,
restoreLibraryItems,
rgbToHex,
round,
roundArrowIcon,
roundToStep,
save,
saveAs,
saveAsJSON,
saveLibraryAsJSON,
sceneCoordsToViewportCoords,
searchIcon,
selectAllIcon,
selectGroup,
selectGroupsForSelectedElements,
selectGroupsFromGivenElements,
serializeAsJSON,
serializeLibraryAsJSON,
setCursor,
setCursorForShape,
setCustomTextMetricsProvider,
setEraserCursor,
share,
sharpArrowIcon,
shouldAllowVerticalAlign,
shouldEnableBindingForPointerEvent,
shouldMaintainAspectRatio,
shouldResizeFromCenter,
shouldRotateWithDiscreteAngle,
shouldShowBoundingBox,
showSelectedShapeActions,
snapDraggedElements,
snapNewElement,
snapResizingElements,
supportsResizeObserver,
suppportsHorizontalAlign,
svgIcon,
syncInvalidIndices,
syncMovedIndices,
tablerCheckIcon,
throttleRAF,
toBrandedType,
toValidURL,
toolIsArrow,
transformElements,
tupleToCoors,
upIcon,
updateActiveTool,
updateBoundElements,
updateElbowArrowPoints,
updateFrameMembershipOfSelectedElements,
updateImageCache,
updateObject,
updateOriginalContainerCache,
updateStable,
usersIcon,
validateFractionalIndices,
vector,
vectorDot,
vectorFromPoint,
vectorNormalize,
vectorScale,
vectorSubtract,
viewportCoordsToSceneCoords,
wrapEvent,
wrapText,
youtubeIcon,
zoomAreaIcon
} from "./chunk-3KPV5WBD.js";
import {
define_import_meta_env_default
} from "./chunk-66VA7UC4.js";
import {
en_default
} from "./chunk-LMHBUWQS.js";
import {
percentages_default
} from "./chunk-MFAYKRVR.js";
import {
__export,
__glob,
__publicField
} from "./chunk-XDFCUUT6.js";
// index.tsx
import React44, { useEffect as useEffect45 } from "react";
// components/InitializeApp.tsx
import { useEffect as useEffect2, useState as useState2 } from "react";
// editor-jotai.ts
import { atom, createStore } from "jotai";
import { createIsolation } from "jotai-scope";
var jotai = createIsolation();
var { useAtom, useSetAtom, useAtomValue, useStore } = jotai;
var EditorJotaiProvider = jotai.Provider;
var editorJotaiStore = createStore();
// import("./locales/**/*.json") in i18n.ts
var globImport_locales_json = __glob({
"./locales/ar-SA.json": () => import("./locales/ar-SA-XORAP2EK.js"),
"./locales/az-AZ.json": () => import("./locales/az-AZ-NAUU3Z4Y.js"),
"./locales/bg-BG.json": () => import("./locales/bg-BG-AAABLFCY.js"),
"./locales/bn-BD.json": () => import("./locales/bn-BD-PM4AC5WG.js"),
"./locales/ca-ES.json": () => import("./locales/ca-ES-YNNMFRQX.js"),
"./locales/cs-CZ.json": () => import("./locales/cs-CZ-DGZA5IKG.js"),
"./locales/da-DK.json": () => import("./locales/da-DK-N76F4QAJ.js"),
"./locales/de-DE.json": () => import("./locales/de-DE-DMRXZ2SZ.js"),
"./locales/el-GR.json": () => import("./locales/el-GR-HIKPLEXI.js"),
"./locales/en.json": () => import("./locales/en-OZCJJ2HN.js"),
"./locales/es-ES.json": () => import("./locales/es-ES-AQYVXC32.js"),
"./locales/eu-ES.json": () => import("./locales/eu-ES-3TOEU5DE.js"),
"./locales/fa-IR.json": () => import("./locales/fa-IR-527GAKUP.js"),
"./locales/fi-FI.json": () => import("./locales/fi-FI-M3WLVDFP.js"),
"./locales/fr-FR.json": () => import("./locales/fr-FR-YE4VDJFI.js"),
"./locales/gl-ES.json": () => import("./locales/gl-ES-KMXUYGUN.js"),
"./locales/he-IL.json": () => import("./locales/he-IL-4MU5N22B.js"),
"./locales/hi-IN.json": () => import("./locales/hi-IN-ZHZNZWFC.js"),
"./locales/hu-HU.json": () => import("./locales/hu-HU-VIYZI3X4.js"),
"./locales/id-ID.json": () => import("./locales/id-ID-22TWZNLA.js"),
"./locales/it-IT.json": () => import("./locales/it-IT-MDEQ2SG3.js"),
"./locales/ja-JP.json": () => import("./locales/ja-JP-K2DI4W6B.js"),
"./locales/kaa.json": () => import("./locales/kaa-6BPSNM3R.js"),
"./locales/kab-KAB.json": () => import("./locales/kab-KAB-2S7ZURK7.js"),
"./locales/kk-KZ.json": () => import("./locales/kk-KZ-UJPYGRQQ.js"),
"./locales/km-KH.json": () => import("./locales/km-KH-M5T5JKUE.js"),
"./locales/ko-KR.json": () => import("./locales/ko-KR-RQX37SNF.js"),
"./locales/ku-TR.json": () => import("./locales/ku-TR-5XJDIERL.js"),
"./locales/lt-LT.json": () => import("./locales/lt-LT-MGUBX6CA.js"),
"./locales/lv-LV.json": () => import("./locales/lv-LV-MD7N5VHD.js"),
"./locales/mr-IN.json": () => import("./locales/mr-IN-4XWMNGQC.js"),
"./locales/my-MM.json": () => import("./locales/my-MM-O4Z74GN5.js"),
"./locales/nb-NO.json": () => import("./locales/nb-NO-BMB73KRH.js"),
"./locales/nl-NL.json": () => import("./locales/nl-NL-F2257BLQ.js"),
"./locales/nn-NO.json": () => import("./locales/nn-NO-NCORG7TS.js"),
"./locales/oc-FR.json": () => import("./locales/oc-FR-ATFBDMF6.js"),
"./locales/pa-IN.json": () => import("./locales/pa-IN-D2I375G4.js"),
"./locales/percentages.json": () => import("./locales/percentages-YKFLWNK2.js"),
"./locales/pl-PL.json": () => import("./locales/pl-PL-YJHOWAAW.js"),
"./locales/pt-BR.json": () => import("./locales/pt-BR-APOPYZJ7.js"),
"./locales/pt-PT.json": () => import("./locales/pt-PT-W56WCN7P.js"),
"./locales/ro-RO.json": () => import("./locales/ro-RO-L575VRQA.js"),
"./locales/ru-RU.json": () => import("./locales/ru-RU-BLG6HZG5.js"),
"./locales/si-LK.json": () => import("./locales/si-LK-KT7GGO6D.js"),
"./locales/sk-SK.json": () => import("./locales/sk-SK-DY6IPO5U.js"),
"./locales/sl-SI.json": () => import("./locales/sl-SI-5DZSRA47.js"),
"./locales/sv-SE.json": () => import("./locales/sv-SE-V32YHALQ.js"),
"./locales/ta-IN.json": () => import("./locales/ta-IN-5JRAGQAO.js"),
"./locales/th-TH.json": () => import("./locales/th-TH-55ACRHDJ.js"),
"./locales/tr-TR.json": () => import("./locales/tr-TR-7QYBXDBO.js"),
"./locales/uk-UA.json": () => import("./locales/uk-UA-TJS2TMRH.js"),
"./locales/vi-VN.json": () => import("./locales/vi-VN-Y5CQ2EKQ.js"),
"./locales/zh-CN.json": () => import("./locales/zh-CN-4MXUOFTH.js"),
"./locales/zh-HK.json": () => import("./locales/zh-HK-RBTGIU3U.js"),
"./locales/zh-TW.json": () => import("./locales/zh-TW-U5VF4CCU.js")
});
// i18n.ts
var COMPLETION_THRESHOLD = 85;
var defaultLang = { code: "en", label: "English" };
var languages = [
defaultLang,
...[
{ code: "ar-SA", label: "\u0627\u0644\u0639\u0631\u0628\u064A\u0629", rtl: true },
{ code: "bg-BG", label: "\u0411\u044A\u043B\u0433\u0430\u0440\u0441\u043A\u0438" },
{ code: "ca-ES", label: "Catal\xE0" },
{ code: "cs-CZ", label: "\u010Cesky" },
{ code: "de-DE", label: "Deutsch" },
{ code: "el-GR", label: "\u0395\u03BB\u03BB\u03B7\u03BD\u03B9\u03BA\u03AC" },
{ code: "es-ES", label: "Espa\xF1ol" },
{ code: "eu-ES", label: "Euskara" },
{ code: "fa-IR", label: "\u0641\u0627\u0631\u0633\u06CC", rtl: true },
{ code: "fi-FI", label: "Suomi" },
{ code: "fr-FR", label: "Fran\xE7ais" },
{ code: "gl-ES", label: "Galego" },
{ code: "he-IL", label: "\u05E2\u05D1\u05E8\u05D9\u05EA", rtl: true },
{ code: "hi-IN", label: "\u0939\u093F\u0928\u094D\u0926\u0940" },
{ code: "hu-HU", label: "Magyar" },
{ code: "id-ID", label: "Bahasa Indonesia" },
{ code: "it-IT", label: "Italiano" },
{ code: "ja-JP", label: "\u65E5\u672C\u8A9E" },
{ code: "kab-KAB", label: "Taqbaylit" },
{ code: "kk-KZ", label: "\u049A\u0430\u0437\u0430\u049B \u0442\u0456\u043B\u0456" },
{ code: "ko-KR", label: "\uD55C\uAD6D\uC5B4" },
{ code: "ku-TR", label: "Kurd\xEE" },
{ code: "lt-LT", label: "Lietuvi\u0173" },
{ code: "lv-LV", label: "Latvie\u0161u" },
{ code: "my-MM", label: "Burmese" },
{ code: "nb-NO", label: "Norsk bokm\xE5l" },
{ code: "nl-NL", label: "Nederlands" },
{ code: "nn-NO", label: "Norsk nynorsk" },
{ code: "oc-FR", label: "Occitan" },
{ code: "pa-IN", label: "\u0A2A\u0A70\u0A1C\u0A3E\u0A2C\u0A40" },
{ code: "pl-PL", label: "Polski" },
{ code: "pt-BR", label: "Portugu\xEAs Brasileiro" },
{ code: "pt-PT", label: "Portugu\xEAs" },
{ code: "ro-RO", label: "Rom\xE2n\u0103" },
{ code: "ru-RU", label: "\u0420\u0443\u0441\u0441\u043A\u0438\u0439" },
{ code: "sk-SK", label: "Sloven\u010Dina" },
{ code: "sv-SE", label: "Svenska" },
{ code: "sl-SI", label: "Sloven\u0161\u010Dina" },
{ code: "tr-TR", label: "T\xFCrk\xE7e" },
{ code: "uk-UA", label: "\u0423\u043A\u0440\u0430\u0457\u043D\u0441\u044C\u043A\u0430" },
{ code: "zh-CN", label: "\u7B80\u4F53\u4E2D\u6587" },
{ code: "zh-TW", label: "\u7E41\u9AD4\u4E2D\u6587" },
{ code: "vi-VN", label: "Ti\u1EBFng Vi\u1EC7t" },
{ code: "mr-IN", label: "\u092E\u0930\u093E\u0920\u0940" }
].filter(
(lang) => percentages_default[lang.code] >= COMPLETION_THRESHOLD
).sort((left, right) => left.label > right.label ? 1 : -1)
];
var TEST_LANG_CODE = "__test__";
if (define_import_meta_env_default.DEV) {
languages.unshift(
{ code: TEST_LANG_CODE, label: "test language" },
{
code: `${TEST_LANG_CODE}.rtl`,
label: "\u202Atest language (rtl)\u202C",
rtl: true
}
);
}
var currentLang = defaultLang;
var currentLangData = {};
var setLanguage = async (lang) => {
currentLang = lang;
document.documentElement.dir = currentLang.rtl ? "rtl" : "ltr";
document.documentElement.lang = currentLang.code;
if (lang.code.startsWith(TEST_LANG_CODE)) {
currentLangData = {};
} else {
try {
currentLangData = await globImport_locales_json(`./locales/${currentLang.code}.json`);
} catch (error) {
console.error(`Failed to load language ${lang.code}:`, error.message);
currentLangData = en_default;
}
}
editorJotaiStore.set(editorLangCodeAtom, lang.code);
};
var getLanguage = () => currentLang;
var findPartsForData = (data, parts) => {
for (let index = 0; index < parts.length; ++index) {
const part = parts[index];
if (data[part] === void 0) {
return void 0;
}
data = data[part];
}
if (typeof data !== "string") {
return void 0;
}
return data;
};
var t = (path, replacement, fallback) => {
if (currentLang.code.startsWith(TEST_LANG_CODE)) {
const name = replacement ? `${path}(${JSON.stringify(replacement).slice(1, -1)})` : path;
return `\u202A[[${name}]]\u202C`;
}
const parts = path.split(".");
let translation = findPartsForData(currentLangData, parts) || findPartsForData(en_default, parts) || fallback;
if (translation === void 0) {
const errorMessage = `Can't find translation for ${path}`;
if (define_import_meta_env_default.PROD) {
console.warn(errorMessage);
return "";
}
throw new Error(errorMessage);
}
if (replacement) {
for (const key in replacement) {
translation = translation.replace(`{{${key}}}`, String(replacement[key]));
}
}
return translation;
};
var editorLangCodeAtom = atom(defaultLang.code);
var useI18n = () => {
const langCode = useAtomValue(editorLangCodeAtom);
return { t, langCode };
};
// components/LoadingMessage.tsx
import { useState, useEffect } from "react";
// components/Spinner.tsx
import React from "react";
import { jsx } from "react/jsx-runtime";
var Spinner = ({
size = "1em",
circleWidth = 8,
synchronized = false,
className = ""
}) => {
const mountTime = React.useRef(Date.now());
const mountDelay = -(mountTime.current % 1600);
return /* @__PURE__ */ jsx("div", { className: `Spinner ${className}`, children: /* @__PURE__ */ jsx(
"svg",
{
viewBox: "0 0 100 100",
style: {
width: size,
height: size,
// fix for remounting causing spinner flicker
["--spinner-delay"]: synchronized ? `${mountDelay}ms` : 0
},
children: /* @__PURE__ */ jsx(
"circle",
{
cx: "50",
cy: "50",
r: 50 - circleWidth / 2,
strokeWidth: circleWidth,
fill: "none",
strokeMiterlimit: "10"
}
)
}
) });
};
var Spinner_default = Spinner;
// components/LoadingMessage.tsx
import clsx from "clsx";
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
var LoadingMessage = ({
delay,
theme
}) => {
const [isWaiting, setIsWaiting] = useState(!!delay);
useEffect(() => {
if (!delay) {
return;
}
const timer = setTimeout(() => {
setIsWaiting(false);
}, delay);
return () => clearTimeout(timer);
}, [delay]);
if (isWaiting) {
return null;
}
return /* @__PURE__ */ jsxs(
"div",
{
className: clsx("LoadingMessage", {
"LoadingMessage--dark": theme === THEME.DARK
}),
children: [
/* @__PURE__ */ jsx2("div", { children: /* @__PURE__ */ jsx2(Spinner_default, {}) }),
/* @__PURE__ */ jsx2("div", { className: "LoadingMessage-text", children: t("labels.loadingScene") })
]
}
);
};
// components/InitializeApp.tsx
import { jsx as jsx3 } from "react/jsx-runtime";
var InitializeApp = (props) => {
const [loading, setLoading] = useState2(true);
useEffect2(() => {
const updateLang = async () => {
await setLanguage(currentLang2);
setLoading(false);
};
const currentLang2 = languages.find((lang) => lang.code === props.langCode) || defaultLang;
updateLang();
}, [props.langCode]);
return loading ? /* @__PURE__ */ jsx3(LoadingMessage, { theme: props.theme }) : props.children;
};
// components/App.tsx
import React43, { useContext as useContext3 } from "react";
import { flushSync as flushSync2 } from "react-dom";
import rough from "roughjs/bin/rough";
import clsx55 from "clsx";
import { nanoid } from "nanoid";
// components/ToolButton.tsx
import React3, { useEffect as useEffect3, useRef, useState as useState3 } from "react";
import clsx2 from "clsx";
import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
var ToolButton = React3.forwardRef(
({
size = "medium",
visible = true,
className = "",
...props
}, ref) => {
const { id: excalId } = useExcalidrawContainer();
const innerRef = React3.useRef(null);
React3.useImperativeHandle(ref, () => innerRef.current);
const sizeCn = `ToolIcon_size_${size}`;
const [isLoading, setIsLoading] = useState3(false);
const isMountedRef = useRef(true);
const onClick = async (event) => {
const ret = "onClick" in props && props.onClick?.(event);
if (isPromiseLike(ret)) {
try {
setIsLoading(true);
await ret;
} catch (error) {
if (!(error instanceof AbortError)) {
throw error;
} else {
console.warn(error);
}
} finally {
if (isMountedRef.current) {
setIsLoading(false);
}
}
}
};
useEffect3(() => {
isMountedRef.current = true;
return () => {
isMountedRef.current = false;
};
}, []);
const lastPointerTypeRef = useRef(null);
if (props.type === "button" || props.type === "icon" || props.type === "submit") {
const type = props.type === "icon" ? "button" : props.type;
return /* @__PURE__ */ jsxs2(
"button",
{
className: clsx2(
"ToolIcon_type_button",
sizeCn,
className,
visible && !props.hidden ? "ToolIcon_type_button--show" : "ToolIcon_type_button--hide",
{
ToolIcon: !props.hidden,
"ToolIcon--selected": props.selected,
"ToolIcon--plain": props.type === "icon"
}
),
style: props.style,
"data-testid": props["data-testid"],
hidden: props.hidden,
title: props.title,
"aria-label": props["aria-label"],
type,
onClick,
ref: innerRef,
disabled: isLoading || props.isLoading || !!props.disabled,
children: [
(props.icon || props.label) && /* @__PURE__ */ jsxs2(
"div",
{
className: "ToolIcon__icon",
"aria-hidden": "true",
"aria-disabled": !!props.disabled,
children: [
props.icon || props.label,
props.keyBindingLabel && /* @__PURE__ */ jsx4("span", { className: "ToolIcon__keybinding", children: props.keyBindingLabel }),
props.isLoading && /* @__PURE__ */ jsx4(Spinner_default, {})
]
}
),
props.showAriaLabel && /* @__PURE__ */ jsxs2("div", { className: "ToolIcon__label", children: [
props["aria-label"],
" ",
isLoading && /* @__PURE__ */ jsx4(Spinner_default, {})
] }),
props.children
]
}
);
}
return /* @__PURE__ */ jsxs2(
"label",
{
className: clsx2("ToolIcon", className),
title: props.title,
onPointerDown: (event) => {
lastPointerTypeRef.current = event.pointerType || null;
props.onPointerDown?.({ pointerType: event.pointerType || null });
},
onPointerUp: () => {
requestAnimationFrame(() => {
lastPointerTypeRef.current = null;
});
},
children: [
/* @__PURE__ */ jsx4(
"input",
{
className: `ToolIcon_type_radio ${sizeCn}`,
type: "radio",
name: props.name,
"aria-label": props["aria-label"],
"aria-keyshortcuts": props["aria-keyshortcuts"],
"data-testid": props["data-testid"],
id: `${excalId}-${props.id}`,
onChange: () => {
props.onChange?.({ pointerType: lastPointerTypeRef.current });
},
checked: props.checked,
ref: innerRef
}
),
/* @__PURE__ */ jsxs2("div", { className: "ToolIcon__icon", children: [
props.icon,
props.keyBindingLabel && /* @__PURE__ */ jsx4("span", { className: "ToolIcon__keybinding", children: props.keyBindingLabel })
] })
]
}
);
}
);
ToolButton.displayName = "ToolButton";
// actions/actionDeleteSelected.tsx
import { jsx as jsx5 } from "react/jsx-runtime";
var deleteSelectedElements = (elements, appState, app) => {
const framesToBeDeleted = new Set(
getSelectedElements(
elements.filter((el) => isFrameLikeElement(el)),
appState
).map((el) => el.id)
);
const selectedElementIds = {};
const elementsMap = app.scene.getNonDeletedElementsMap();
const processedElements = /* @__PURE__ */ new Set();
for (const frameId of framesToBeDeleted) {
const frameChildren = getFrameChildren(elements, frameId);
for (const el of frameChildren) {
if (processedElements.has(el.id)) {
continue;
}
if (isBoundToContainer(el)) {
const containerElement = getContainerElement(el, elementsMap);
if (containerElement) {
selectedElementIds[containerElement.id] = true;
}
} else {
selectedElementIds[el.id] = true;
}
processedElements.add(el.id);
}
}
let shouldSelectEditingGroup = true;
const nextElements = elements.map((el) => {
if (appState.selectedElementIds[el.id]) {
const boundElement = isBoundToContainer(el) ? getContainerElement(el, elementsMap) : null;
if (el.frameId && framesToBeDeleted.has(el.frameId)) {
shouldSelectEditingGroup = false;
selectedElementIds[el.id] = true;
return el;
}
if (boundElement?.frameId && framesToBeDeleted.has(boundElement?.frameId)) {
return el;
}
if (el.boundElements) {
el.boundElements.forEach((candidate) => {
const bound = app.scene.getNonDeletedElementsMap().get(candidate.id);
if (bound && isElbowArrow(bound)) {
mutateElement(bound, {
startBinding: el.id === bound.startBinding?.elementId ? null : bound.startBinding,
endBinding: el.id === bound.endBinding?.elementId ? null : bound.endBinding
});
mutateElement(bound, { points: bound.points });
}
});
}
return newElementWith(el, { isDeleted: true });
}
if (el.frameId && framesToBeDeleted.has(el.frameId)) {
shouldSelectEditingGroup = false;
if (!isBoundToContainer(el)) {
selectedElementIds[el.id] = true;
}
return newElementWith(el, { frameId: null });
}
if (isBoundToContainer(el) && appState.selectedElementIds[el.containerId]) {
return newElementWith(el, { isDeleted: true });
}
return el;
});
let nextEditingGroupId = appState.editingGroupId;
if (shouldSelectEditingGroup && appState.editingGroupId) {
const elems = getElementsInGroup(
nextElements,
appState.editingGroupId
).filter((el) => !el.isDeleted);
if (elems.length > 1) {
if (elems[0]) {
selectedElementIds[elems[0].id] = true;
}
} else {
nextEditingGroupId = null;
if (elems[0]) {
selectedElementIds[elems[0].id] = true;
}
const lastElementInGroup = elems[0];
if (lastElementInGroup) {
const editingGroupIdx = lastElementInGroup.groupIds.findIndex(
(groupId) => {
return groupId === appState.editingGroupId;
}
);
const superGroupId = lastElementInGroup.groupIds[editingGroupIdx + 1];
if (superGroupId) {
const elems2 = getElementsInGroup(nextElements, superGroupId).filter(
(el) => !el.isDeleted
);
if (elems2.length > 1) {
nextEditingGroupId = superGroupId;
elems2.forEach((el) => {
selectedElementIds[el.id] = true;
});
}
}
}
}
}
return {
elements: nextElements,
appState: {
...appState,
...selectGroupsForSelectedElements(
{
selectedElementIds,
editingGroupId: nextEditingGroupId
},
nextElements,
appState,
null
)
}
};
};
var handleGroupEditingState = (appState, elements) => {
if (appState.editingGroupId) {
const siblingElements = getElementsInGroup(
getNonDeletedElements(elements),
appState.editingGroupId
);
if (siblingElements.length) {
return {
...appState,
selectedElementIds: { [siblingElements[0].id]: true }
};
}
}
return appState;
};
var actionDeleteSelected = register({
name: "deleteSelectedElements",
label: "labels.delete",
icon: TrashIcon,
trackEvent: { category: "element", action: "delete" },
perform: (elements, appState, formData, app) => {
if (appState.editingLinearElement) {
const {
elementId,
selectedPointsIndices,
startBindingElement,
endBindingElement
} = appState.editingLinearElement;
const elementsMap = app.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) {
return false;
}
if (selectedPointsIndices == null) {
return false;
}
if (element.points.length < 2) {
const nextElements2 = elements.map((el) => {
if (el.id === element.id) {
return newElementWith(el, { isDeleted: true });
}
return el;
});
const nextAppState2 = handleGroupEditingState(appState, nextElements2);
return {
elements: nextElements2,
appState: {
...nextAppState2,
editingLinearElement: null
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
const binding = {
startBindingElement: selectedPointsIndices?.includes(0) ? null : startBindingElement,
endBindingElement: selectedPointsIndices?.includes(
element.points.length - 1
) ? null : endBindingElement
};
LinearElementEditor.deletePoints(element, selectedPointsIndices);
return {
elements,
appState: {
...appState,
editingLinearElement: {
...appState.editingLinearElement,
...binding,
selectedPointsIndices: selectedPointsIndices?.[0] > 0 ? [selectedPointsIndices[0] - 1] : [0]
}
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
let { elements: nextElements, appState: nextAppState } = deleteSelectedElements(elements, appState, app);
fixBindingsAfterDeletion(
nextElements,
nextElements.filter((el) => el.isDeleted)
);
nextAppState = handleGroupEditingState(nextAppState, nextElements);
return {
elements: nextElements,
appState: {
...nextAppState,
activeTool: updateActiveTool(appState, { type: "selection" }),
multiElement: null,
activeEmbeddable: null
},
captureUpdate: isSomeElementSelected(
getNonDeletedElements(elements),
appState
) ? CaptureUpdateAction.IMMEDIATELY : CaptureUpdateAction.EVENTUALLY
};
},
keyTest: (event, appState, elements) => (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) && !event[KEYS.CTRL_OR_CMD],
PanelComponent: ({ elements, appState, updateData }) => /* @__PURE__ */ jsx5(
ToolButton,
{
type: "button",
icon: TrashIcon,
title: t("labels.delete"),
"aria-label": t("labels.delete"),
onClick: () => updateData(null),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
// zindex.ts
var isOfTargetFrame = (element, frameId) => {
return element.frameId === frameId || element.id === frameId;
};
var getIndicesToMove = (elements, appState, elementsToBeMoved) => {
let selectedIndices = [];
let deletedIndices = [];
let includeDeletedIndex = null;
let index = -1;
const selectedElementIds = arrayToMap(
elementsToBeMoved ? elementsToBeMoved : getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true
})
);
while (++index < elements.length) {
const element = elements[index];
if (selectedElementIds.get(element.id)) {
if (deletedIndices.length) {
selectedIndices = selectedIndices.concat(deletedIndices);
deletedIndices = [];
}
selectedIndices.push(index);
includeDeletedIndex = index + 1;
} else if (element.isDeleted && includeDeletedIndex === index) {
includeDeletedIndex = index + 1;
deletedIndices.push(index);
} else {
deletedIndices = [];
}
}
return selectedIndices;
};
var toContiguousGroups = (array) => {
let cursor = 0;
return array.reduce((acc, value, index) => {
if (index > 0 && array[index - 1] !== value - 1) {
cursor = ++cursor;
}
(acc[cursor] || (acc[cursor] = [])).push(value);
return acc;
}, []);
};
var getTargetIndexAccountingForBinding = (nextElement, elements, direction) => {
if ("containerId" in nextElement && nextElement.containerId) {
const containerElement = Scene_default.getScene(nextElement).getElement(
nextElement.containerId
);
if (containerElement) {
return direction === "left" ? Math.min(
elements.indexOf(containerElement),
elements.indexOf(nextElement)
) : Math.max(
elements.indexOf(containerElement),
elements.indexOf(nextElement)
);
}
} else {
const boundElementId = nextElement.boundElements?.find(
(binding) => binding.type !== "arrow"
)?.id;
if (boundElementId) {
const boundTextElement = Scene_default.getScene(nextElement).getElement(boundElementId);
if (boundTextElement) {
return direction === "left" ? Math.min(
elements.indexOf(boundTextElement),
elements.indexOf(nextElement)
) : Math.max(
elements.indexOf(boundTextElement),
elements.indexOf(nextElement)
);
}
}
}
};
var getContiguousFrameRangeElements = (allElements, frameId) => {
let rangeStart = -1;
let rangeEnd = -1;
allElements.forEach((element, index) => {
if (isOfTargetFrame(element, frameId)) {
if (rangeStart === -1) {
rangeStart = index;
}
rangeEnd = index;
}
});
if (rangeStart === -1) {
return [];
}
return allElements.slice(rangeStart, rangeEnd + 1);
};
var getTargetIndex = (appState, elements, boundaryIndex, direction, containingFrame) => {
const sourceElement = elements[boundaryIndex];
const indexFilter = (element) => {
if (element.isDeleted) {
return false;
}
if (containingFrame) {
return element.frameId === containingFrame;
}
if (appState.editingGroupId) {
return element.groupIds.includes(appState.editingGroupId);
}
return true;
};
const candidateIndex = direction === "left" ? findLastIndex(
elements,
(el) => indexFilter(el),
Math.max(0, boundaryIndex - 1)
) : findIndex(elements, (el) => indexFilter(el), boundaryIndex + 1);
const nextElement = elements[candidateIndex];
if (!nextElement) {
return -1;
}
if (appState.editingGroupId) {
if (
// candidate element is a sibling in current editing group → return
sourceElement?.groupIds.join("") === nextElement?.groupIds.join("")
) {
return getTargetIndexAccountingForBinding(nextElement, elements, direction) ?? candidateIndex;
} else if (!nextElement?.groupIds.includes(appState.editingGroupId)) {
return -1;
}
}
if (!containingFrame && (nextElement.frameId || isFrameLikeElement(nextElement))) {
const frameElements = getContiguousFrameRangeElements(
elements,
nextElement.frameId || nextElement.id
);
return direction === "left" ? elements.indexOf(frameElements[0]) : elements.indexOf(frameElements[frameElements.length - 1]);
}
if (!nextElement.groupIds.length) {
return getTargetIndexAccountingForBinding(nextElement, elements, direction) ?? candidateIndex;
}
const siblingGroupId = appState.editingGroupId ? nextElement.groupIds[nextElement.groupIds.indexOf(appState.editingGroupId) - 1] : nextElement.groupIds[nextElement.groupIds.length - 1];
const elementsInSiblingGroup = getElementsInGroup(elements, siblingGroupId);
if (elementsInSiblingGroup.length) {
return direction === "left" ? elements.indexOf(elementsInSiblingGroup[0]) : elements.indexOf(
elementsInSiblingGroup[elementsInSiblingGroup.length - 1]
);
}
return candidateIndex;
};
var getTargetElementsMap = (elements, indices) => {
return indices.reduce((acc, index) => {
const element = elements[index];
acc.set(element.id, element);
return acc;
}, /* @__PURE__ */ new Map());
};
var shiftElementsByOne = (elements, appState, direction) => {
const indicesToMove = getIndicesToMove(elements, appState);
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
let groupedIndices = toContiguousGroups(indicesToMove);
if (direction === "right") {
groupedIndices = groupedIndices.reverse();
}
const selectedFrames = new Set(
indicesToMove.filter((idx) => isFrameLikeElement(elements[idx])).map((idx) => elements[idx].id)
);
groupedIndices.forEach((indices, i) => {
const leadingIndex = indices[0];
const trailingIndex = indices[indices.length - 1];
const boundaryIndex = direction === "left" ? leadingIndex : trailingIndex;
const containingFrame = indices.some((idx) => {
const el = elements[idx];
return el.frameId && selectedFrames.has(el.frameId);
}) ? null : elements[boundaryIndex]?.frameId;
const targetIndex = getTargetIndex(
appState,
elements,
boundaryIndex,
direction,
containingFrame
);
if (targetIndex === -1 || boundaryIndex === targetIndex) {
return;
}
const leadingElements = direction === "left" ? elements.slice(0, targetIndex) : elements.slice(0, leadingIndex);
const targetElements = elements.slice(leadingIndex, trailingIndex + 1);
const displacedElements = direction === "left" ? elements.slice(targetIndex, leadingIndex) : elements.slice(trailingIndex + 1, targetIndex + 1);
const trailingElements = direction === "left" ? elements.slice(trailingIndex + 1) : elements.slice(targetIndex + 1);
elements = direction === "left" ? [
...leadingElements,
...targetElements,
...displacedElements,
...trailingElements
] : [
...leadingElements,
...displacedElements,
...targetElements,
...trailingElements
];
});
syncMovedIndices(elements, targetElementsMap);
return elements;
};
var shiftElementsToEnd = (elements, appState, direction, containingFrame, elementsToBeMoved) => {
const indicesToMove = getIndicesToMove(elements, appState, elementsToBeMoved);
const targetElementsMap = getTargetElementsMap(elements, indicesToMove);
const displacedElements = [];
let leadingIndex;
let trailingIndex;
if (direction === "left") {
if (containingFrame) {
leadingIndex = findIndex(
elements,
(el) => isOfTargetFrame(el, containingFrame)
);
} else if (appState.editingGroupId) {
const groupElements = getElementsInGroup(
elements,
appState.editingGroupId
);
if (!groupElements.length) {
return elements;
}
leadingIndex = elements.indexOf(groupElements[0]);
} else {
leadingIndex = 0;
}
trailingIndex = indicesToMove[indicesToMove.length - 1];
} else {
if (containingFrame) {
trailingIndex = findLastIndex(
elements,
(el) => isOfTargetFrame(el, containingFrame)
);
} else if (appState.editingGroupId) {
const groupElements = getElementsInGroup(
elements,
appState.editingGroupId
);
if (!groupElements.length) {
return elements;
}
trailingIndex = elements.indexOf(groupElements[groupElements.length - 1]);
} else {
trailingIndex = elements.length - 1;
}
leadingIndex = indicesToMove[0];
}
if (leadingIndex === -1) {
leadingIndex = 0;
}
for (let index = leadingIndex; index < trailingIndex + 1; index++) {
if (!indicesToMove.includes(index)) {
displacedElements.push(elements[index]);
}
}
const targetElements = Array.from(targetElementsMap.values());
const leadingElements = elements.slice(0, leadingIndex);
const trailingElements = elements.slice(trailingIndex + 1);
const nextElements = direction === "left" ? [
...leadingElements,
...targetElements,
...displacedElements,
...trailingElements
] : [
...leadingElements,
...displacedElements,
...targetElements,
...trailingElements
];
syncMovedIndices(nextElements, targetElementsMap);
return nextElements;
};
function shiftElementsAccountingForFrames(allElements, appState, direction, shiftFunction) {
const elementsToMove = arrayToMap(
getSelectedElements(allElements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true
})
);
const frameAwareContiguousElementsToMove = { regularElements: [], frameChildren: /* @__PURE__ */ new Map() };
const fullySelectedFrames = /* @__PURE__ */ new Set();
for (const element of allElements) {
if (elementsToMove.has(element.id) && isFrameLikeElement(element)) {
fullySelectedFrames.add(element.id);
}
}
for (const element of allElements) {
if (elementsToMove.has(element.id)) {
if (isFrameLikeElement(element) || element.frameId && fullySelectedFrames.has(element.frameId)) {
frameAwareContiguousElementsToMove.regularElements.push(element);
} else if (!element.frameId) {
frameAwareContiguousElementsToMove.regularElements.push(element);
} else {
const frameChildren = frameAwareContiguousElementsToMove.frameChildren.get(
element.frameId
) || [];
frameChildren.push(element);
frameAwareContiguousElementsToMove.frameChildren.set(
element.frameId,
frameChildren
);
}
}
}
let nextElements = allElements;
const frameChildrenSets = Array.from(
frameAwareContiguousElementsToMove.frameChildren.entries()
);
for (const [frameId, children] of frameChildrenSets) {
nextElements = shiftFunction(
allElements,
appState,
direction,
frameId,
children
);
}
return shiftFunction(
nextElements,
appState,
direction,
null,
frameAwareContiguousElementsToMove.regularElements
);
}
var moveOneLeft = (allElements, appState) => {
return shiftElementsByOne(allElements, appState, "left");
};
var moveOneRight = (allElements, appState) => {
return shiftElementsByOne(allElements, appState, "right");
};
var moveAllLeft = (allElements, appState) => {
return shiftElementsAccountingForFrames(
allElements,
appState,
"left",
shiftElementsToEnd
);
};
var moveAllRight = (allElements, appState) => {
return shiftElementsAccountingForFrames(
allElements,
appState,
"right",
shiftElementsToEnd
);
};
// actions/actionZindex.tsx
import { jsx as jsx6 } from "react/jsx-runtime";
var actionSendBackward = register({
name: "sendBackward",
label: "labels.sendBackward",
keywords: ["move down", "zindex", "layer"],
icon: SendBackwardIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
return {
elements: moveOneLeft(elements, appState),
appState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyPriority: 40,
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && !event.shiftKey && event.code === CODES.BRACKET_LEFT,
PanelComponent: ({ updateData, appState }) => /* @__PURE__ */ jsx6(
"button",
{
type: "button",
className: "zIndexButton",
onClick: () => updateData(null),
title: `${t("labels.sendBackward")} \u2014 ${getShortcutKey("CtrlOrCmd+[")}`,
children: SendBackwardIcon
}
)
});
var actionBringForward = register({
name: "bringForward",
label: "labels.bringForward",
keywords: ["move up", "zindex", "layer"],
icon: BringForwardIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
return {
elements: moveOneRight(elements, appState),
appState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyPriority: 40,
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && !event.shiftKey && event.code === CODES.BRACKET_RIGHT,
PanelComponent: ({ updateData, appState }) => /* @__PURE__ */ jsx6(
"button",
{
type: "button",
className: "zIndexButton",
onClick: () => updateData(null),
title: `${t("labels.bringForward")} \u2014 ${getShortcutKey("CtrlOrCmd+]")}`,
children: BringForwardIcon
}
)
});
var actionSendToBack = register({
name: "sendToBack",
label: "labels.sendToBack",
keywords: ["move down", "zindex", "layer"],
icon: SendToBackIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
return {
elements: moveAllLeft(elements, appState),
appState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => isDarwin ? event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.BRACKET_LEFT : event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.code === CODES.BRACKET_LEFT,
PanelComponent: ({ updateData, appState }) => /* @__PURE__ */ jsx6(
"button",
{
type: "button",
className: "zIndexButton",
onClick: () => updateData(null),
title: `${t("labels.sendToBack")} \u2014 ${isDarwin ? getShortcutKey("CtrlOrCmd+Alt+[") : getShortcutKey("CtrlOrCmd+Shift+[")}`,
children: SendToBackIcon
}
)
});
var actionBringToFront = register({
name: "bringToFront",
label: "labels.bringToFront",
keywords: ["move up", "zindex", "layer"],
icon: BringToFrontIcon,
trackEvent: { category: "element" },
perform: (elements, appState) => {
return {
elements: moveAllRight(elements, appState),
appState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => isDarwin ? event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.BRACKET_RIGHT : event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.code === CODES.BRACKET_RIGHT,
PanelComponent: ({ updateData, appState }) => /* @__PURE__ */ jsx6(
"button",
{
type: "button",
className: "zIndexButton",
onClick: (event) => updateData(null),
title: `${t("labels.bringToFront")} \u2014 ${isDarwin ? getShortcutKey("CtrlOrCmd+Alt+]") : getShortcutKey("CtrlOrCmd+Shift+]")}`,
children: BringToFrontIcon
}
)
});
// actions/actionSelectAll.ts
var actionSelectAll = register({
name: "selectAll",
label: "labels.selectAll",
icon: selectAllIcon,
trackEvent: { category: "canvas" },
viewMode: false,
perform: (elements, appState, value, app) => {
if (appState.editingLinearElement) {
return false;
}
const selectedElementIds = elements.filter(
(element) => !element.isDeleted && !(isTextElement(element) && element.containerId) && !element.locked
).reduce((map, element) => {
map[element.id] = true;
return map;
}, {});
return {
appState: {
...appState,
...selectGroupsForSelectedElements(
{
editingGroupId: null,
selectedElementIds
},
getNonDeletedElements(elements),
appState,
app
),
selectedLinearElement: (
// single linear element selected
Object.keys(selectedElementIds).length === 1 && isLinearElement(elements[0]) ? new LinearElementEditor(elements[0]) : null
)
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.A
});
// element/sortElements.ts
var normalizeGroupElementOrder = (elements) => {
const origElements = elements.slice();
const sortedElements = /* @__PURE__ */ new Set();
const orderInnerGroups = (elements2) => {
const firstGroupSig = elements2[0]?.groupIds?.join("");
const aGroup = [elements2[0]];
const bGroup = [];
for (const element of elements2.slice(1)) {
if (element.groupIds?.join("") === firstGroupSig) {
aGroup.push(element);
} else {
bGroup.push(element);
}
}
return bGroup.length ? [...aGroup, ...orderInnerGroups(bGroup)] : aGroup;
};
const groupHandledElements = /* @__PURE__ */ new Map();
origElements.forEach((element, idx) => {
if (groupHandledElements.has(element.id)) {
return;
}
if (element.groupIds?.length) {
const topGroup = element.groupIds[element.groupIds.length - 1];
const groupElements = origElements.slice(idx).filter((element2) => {
const ret = element2?.groupIds?.some((id) => id === topGroup);
if (ret) {
groupHandledElements.set(element2.id, true);
}
return ret;
});
for (const elem of orderInnerGroups(groupElements)) {
sortedElements.add(elem);
}
} else {
sortedElements.add(element);
}
});
if (sortedElements.size !== elements.length) {
console.error("normalizeGroupElementOrder: lost some elements... bailing!");
return elements;
}
return [...sortedElements];
};
var normalizeBoundElementsOrder = (elements) => {
const elementsMap = arrayToMapWithIndex(elements);
const origElements = elements.slice();
const sortedElements = /* @__PURE__ */ new Set();
origElements.forEach((element, idx) => {
if (!element) {
return;
}
if (element.boundElements?.length) {
sortedElements.add(element);
origElements[idx] = null;
element.boundElements.forEach((boundElement) => {
const child = elementsMap.get(boundElement.id);
if (child && boundElement.type === "text") {
sortedElements.add(child[0]);
origElements[child[1]] = null;
}
});
} else if (element.type === "text" && element.containerId) {
const parent = elementsMap.get(element.containerId);
if (!parent?.[0].boundElements?.find((x) => x.id === element.id)) {
sortedElements.add(element);
origElements[idx] = null;
}
} else {
sortedElements.add(element);
origElements[idx] = null;
}
});
if (sortedElements.size !== elements.length) {
console.error(
"normalizeBoundElementsOrder: lost some elements... bailing!"
);
return elements;
}
return [...sortedElements];
};
var normalizeElementOrder = (elements) => {
return normalizeBoundElementsOrder(normalizeGroupElementOrder(elements));
};
// actions/actionDuplicateSelection.tsx
import { jsx as jsx7 } from "react/jsx-runtime";
var actionDuplicateSelection = register({
name: "duplicateSelection",
label: "labels.duplicateSelection",
icon: DuplicateIcon,
trackEvent: { category: "element" },
perform: (elements, appState, formData, app) => {
if (appState.editingLinearElement) {
try {
const newAppState = LinearElementEditor.duplicateSelectedPoints(
appState,
app.scene.getNonDeletedElementsMap()
);
return {
elements,
appState: newAppState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
} catch {
return false;
}
}
const nextState = duplicateElements2(elements, appState);
if (app.props.onDuplicate && nextState.elements) {
const mappedElements = app.props.onDuplicate(
nextState.elements,
elements
);
if (mappedElements) {
nextState.elements = mappedElements;
}
}
return {
...nextState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.D,
PanelComponent: ({ elements, appState, updateData }) => /* @__PURE__ */ jsx7(
ToolButton,
{
type: "button",
icon: DuplicateIcon,
title: `${t("labels.duplicateSelection")} \u2014 ${getShortcutKey(
"CtrlOrCmd+D"
)}`,
"aria-label": t("labels.duplicateSelection"),
onClick: () => updateData(null),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
var duplicateElements2 = (elements, appState) => {
const groupIdMap = /* @__PURE__ */ new Map();
const newElements = [];
const oldElements = [];
const oldIdToDuplicatedId = /* @__PURE__ */ new Map();
const duplicatedElementsMap = /* @__PURE__ */ new Map();
const elementsMap = arrayToMap(elements);
const duplicateAndOffsetElement = (element) => {
const elements2 = castArray(element);
const _newElements = elements2.reduce(
(acc, element2) => {
if (processedIds.has(element2.id)) {
return acc;
}
processedIds.set(element2.id, true);
const newElement2 = duplicateElement(
appState.editingGroupId,
groupIdMap,
element2,
{
x: element2.x + DEFAULT_GRID_SIZE / 2,
y: element2.y + DEFAULT_GRID_SIZE / 2
}
);
processedIds.set(newElement2.id, true);
duplicatedElementsMap.set(newElement2.id, newElement2);
oldIdToDuplicatedId.set(element2.id, newElement2.id);
oldElements.push(element2);
newElements.push(newElement2);
acc.push(newElement2);
return acc;
},
[]
);
return Array.isArray(element) ? _newElements : _newElements[0] || null;
};
elements = normalizeElementOrder(elements);
const idsOfElementsToDuplicate = arrayToMap(
getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true
})
);
const processedIds = /* @__PURE__ */ new Map();
const elementsWithClones = elements.slice();
const insertAfterIndex = (index, elements2) => {
invariant(index !== -1, "targetIndex === -1 ");
if (!Array.isArray(elements2) && !elements2) {
return;
}
elementsWithClones.splice(index + 1, 0, ...castArray(elements2));
};
const frameIdsToDuplicate = new Set(
elements.filter(
(el) => idsOfElementsToDuplicate.has(el.id) && isFrameLikeElement(el)
).map((el) => el.id)
);
for (const element of elements) {
if (processedIds.has(element.id)) {
continue;
}
if (!idsOfElementsToDuplicate.has(element.id)) {
continue;
}
const groupId = getSelectedGroupForElement(appState, element);
if (groupId) {
const groupElements = getElementsInGroup(elements, groupId).flatMap(
(element2) => isFrameLikeElement(element2) ? [...getFrameChildren(elements, element2.id), element2] : [element2]
);
const targetIndex = findLastIndex(elementsWithClones, (el) => {
return el.groupIds?.includes(groupId);
});
insertAfterIndex(targetIndex, duplicateAndOffsetElement(groupElements));
continue;
}
if (element.frameId && frameIdsToDuplicate.has(element.frameId)) {
continue;
}
if (isFrameLikeElement(element)) {
const frameId = element.id;
const frameChildren = getFrameChildren(elements, frameId);
const targetIndex = findLastIndex(elementsWithClones, (el) => {
return el.frameId === frameId || el.id === frameId;
});
insertAfterIndex(
targetIndex,
duplicateAndOffsetElement([...frameChildren, element])
);
continue;
}
if (hasBoundTextElement(element)) {
const boundTextElement = getBoundTextElement(element, elementsMap);
const targetIndex = findLastIndex(elementsWithClones, (el) => {
return el.id === element.id || "containerId" in el && el.containerId === element.id;
});
if (boundTextElement) {
insertAfterIndex(
targetIndex,
duplicateAndOffsetElement([element, boundTextElement])
);
} else {
insertAfterIndex(targetIndex, duplicateAndOffsetElement(element));
}
continue;
}
if (isBoundToContainer(element)) {
const container = getContainerElement(element, elementsMap);
const targetIndex = findLastIndex(elementsWithClones, (el) => {
return el.id === element.id || el.id === container?.id;
});
if (container) {
insertAfterIndex(
targetIndex,
duplicateAndOffsetElement([container, element])
);
} else {
insertAfterIndex(targetIndex, duplicateAndOffsetElement(element));
}
continue;
}
insertAfterIndex(
findLastIndex(elementsWithClones, (el) => el.id === element.id),
duplicateAndOffsetElement(element)
);
}
bindTextToShapeAfterDuplication(
elementsWithClones,
oldElements,
oldIdToDuplicatedId
);
fixBindingsAfterDuplication(
elementsWithClones,
oldElements,
oldIdToDuplicatedId
);
bindElementsToFramesAfterDuplication(
elementsWithClones,
oldElements,
oldIdToDuplicatedId
);
const nextElementsToSelect = excludeElementsInFramesFromSelection(newElements);
return {
elements: elementsWithClones,
appState: {
...appState,
...selectGroupsForSelectedElements(
{
editingGroupId: appState.editingGroupId,
selectedElementIds: nextElementsToSelect.reduce(
(acc, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
},
{}
)
},
getNonDeletedElements(elementsWithClones),
appState,
null
)
}
};
};
// actions/actionProperties.tsx
import { useEffect as useEffect15, useMemo as useMemo4, useRef as useRef11, useState as useState8 } from "react";
// analytics.ts
var ALLOWED_CATEGORIES_TO_TRACK = /* @__PURE__ */ new Set(["command_palette", "export"]);
var trackEvent = (category, action, label, value) => {
try {
if (typeof window === "undefined" || define_import_meta_env_default.VITE_WORKER_ID || define_import_meta_env_default.VITE_APP_ENABLE_TRACKING !== "true") {
return;
}
if (!ALLOWED_CATEGORIES_TO_TRACK.has(category)) {
return;
}
if (define_import_meta_env_default.DEV) {
return;
}
if (!define_import_meta_env_default.PROD) {
console.info("trackEvent", { category, action, label, value });
}
if (window.sa_event) {
window.sa_event(action, {
category,
label,
value
});
}
} catch (error) {
console.error("error during analytics", error);
}
};
// components/ButtonIconSelect.tsx
import clsx4 from "clsx";
// components/ButtonIcon.tsx
import { forwardRef } from "react";
import clsx3 from "clsx";
import { jsx as jsx8 } from "react/jsx-runtime";
var ButtonIcon = forwardRef(
(props, ref) => {
const { title, className, testId, active, standalone, icon, onClick } = props;
return /* @__PURE__ */ jsx8(
"button",
{
type: "button",
ref,
title,
"data-testid": testId,
className: clsx3(className, { standalone, active }),
onClick,
children: icon
},
title
);
}
);
// components/ButtonIconSelect.tsx
import { jsx as jsx9, jsxs as jsxs3 } from "react/jsx-runtime";
var ButtonIconSelect = (props) => /* @__PURE__ */ jsx9("div", { className: "buttonList", children: props.options.map(
(option) => props.type === "button" ? /* @__PURE__ */ jsx9(
ButtonIcon,
{
icon: option.icon,
title: option.text,
testId: option.testId,
active: option.active ?? props.value === option.value,
onClick: (event) => props.onClick(option.value, event)
},
option.text
) : /* @__PURE__ */ jsxs3(
"label",
{
className: clsx4({ active: props.value === option.value }),
title: option.text,
children: [
/* @__PURE__ */ jsx9(
"input",
{
type: "radio",
name: props.group,
onChange: () => props.onChange(option.value),
checked: props.value === option.value,
"data-testid": option.testId
}
),
option.icon
]
},
option.text
)
) });
// components/ColorPicker/TopPicks.tsx
import clsx5 from "clsx";
import { jsx as jsx10 } from "react/jsx-runtime";
var TopPicks = ({
onChange,
type,
activeColor,
topPicks
}) => {
let colors;
if (type === "elementStroke") {
colors = DEFAULT_ELEMENT_STROKE_PICKS;
}
if (type === "elementBackground") {
colors = DEFAULT_ELEMENT_BACKGROUND_PICKS;
}
if (type === "canvasBackground") {
colors = DEFAULT_CANVAS_BACKGROUND_PICKS;
}
if (topPicks) {
colors = topPicks;
}
if (!colors) {
console.error("Invalid type for TopPicks");
return null;
}
return /* @__PURE__ */ jsx10("div", { className: "color-picker__top-picks", children: colors.map((color) => /* @__PURE__ */ jsx10(
"button",
{
className: clsx5("color-picker__button", {
active: color === activeColor,
"is-transparent": color === "transparent" || !color
}),
style: { "--swatch-color": color },
type: "button",
title: color,
onClick: () => onChange(color),
"data-testid": `color-top-pick-${color}`,
children: /* @__PURE__ */ jsx10("div", { className: "color-picker__button-outline" })
},
color
)) });
};
// components/ButtonSeparator.tsx
import { jsx as jsx11 } from "react/jsx-runtime";
var ButtonSeparator = () => /* @__PURE__ */ jsx11(
"div",
{
style: {
width: 1,
height: "1rem",
backgroundColor: "var(--default-border-color)",
margin: "0 auto"
}
}
);
// components/ColorPicker/Picker.tsx
import React4, { useEffect as useEffect7, useState as useState4 } from "react";
// components/ColorPicker/ShadeList.tsx
import clsx6 from "clsx";
import { useEffect as useEffect4, useRef as useRef2 } from "react";
// components/ColorPicker/colorPickerUtils.ts
var getColorNameAndShadeFromColor = ({
palette: palette2,
color
}) => {
for (const [colorName, colorVal] of Object.entries(palette2)) {
if (Array.isArray(colorVal)) {
const shade = colorVal.indexOf(color);
if (shade > -1) {
return { colorName, shade };
}
} else if (colorVal === color) {
return { colorName, shade: null };
}
}
return null;
};
var colorPickerHotkeyBindings = [
["q", "w", "e", "r", "t"],
["a", "s", "d", "f", "g"],
["z", "x", "c", "v", "b"]
].flat();
var isCustomColor = ({
color,
palette: palette2
}) => {
const paletteValues = Object.values(palette2).flat();
return !paletteValues.includes(color);
};
var getMostUsedCustomColors = (elements, type, palette2) => {
const elementColorTypeMap = {
elementBackground: "backgroundColor",
elementStroke: "strokeColor"
};
const colors = elements.filter((element) => {
if (element.isDeleted) {
return false;
}
const color = element[elementColorTypeMap[type]];
return isCustomColor({ color, palette: palette2 });
});
const colorCountMap = /* @__PURE__ */ new Map();
colors.forEach((element) => {
const color = element[elementColorTypeMap[type]];
if (colorCountMap.has(color)) {
colorCountMap.set(color, colorCountMap.get(color) + 1);
} else {
colorCountMap.set(color, 1);
}
});
return [...colorCountMap.entries()].sort((a, b) => b[1] - a[1]).map((c) => c[0]).slice(0, MAX_CUSTOM_COLORS_USED_IN_CANVAS);
};
var activeColorPickerSectionAtom = atom(null);
var calculateContrast = (r, g, b) => {
const yiq = (r * 299 + g * 587 + b * 114) / 1e3;
return yiq >= 160 ? "black" : "white";
};
var getContrastYIQ = (bgHex, isCustomColor2) => {
if (isCustomColor2) {
const style = new Option().style;
style.color = bgHex;
if (style.color) {
const rgb = style.color.replace(/^(rgb|rgba)\(/, "").replace(/\)$/, "").replace(/\s/g, "").split(",");
const r2 = parseInt(rgb[0]);
const g2 = parseInt(rgb[1]);
const b2 = parseInt(rgb[2]);
return calculateContrast(r2, g2, b2);
}
}
if (bgHex === "transparent") {
return "black";
}
const r = parseInt(bgHex.substring(1, 3), 16);
const g = parseInt(bgHex.substring(3, 5), 16);
const b = parseInt(bgHex.substring(5, 7), 16);
return calculateContrast(r, g, b);
};
// components/ColorPicker/HotkeyLabel.tsx
import { jsxs as jsxs4 } from "react/jsx-runtime";
var HotkeyLabel = ({
color,
keyLabel,
isCustomColor: isCustomColor2 = false,
isShade = false
}) => {
return /* @__PURE__ */ jsxs4(
"div",
{
className: "color-picker__button__hotkey-label",
style: {
color: getContrastYIQ(color, isCustomColor2)
},
children: [
isShade && "\u21E7",
keyLabel
]
}
);
};
var HotkeyLabel_default = HotkeyLabel;
// components/ColorPicker/ShadeList.tsx
import { jsx as jsx12, jsxs as jsxs5 } from "react/jsx-runtime";
var ShadeList = ({ hex, onChange, palette: palette2 }) => {
const colorObj = getColorNameAndShadeFromColor({
color: hex || "transparent",
palette: palette2
});
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
activeColorPickerSectionAtom
);
const btnRef = useRef2(null);
useEffect4(() => {
if (btnRef.current && activeColorPickerSection === "shades") {
btnRef.current.focus();
}
}, [colorObj, activeColorPickerSection]);
if (colorObj) {
const { colorName, shade } = colorObj;
const shades = palette2[colorName];
if (Array.isArray(shades)) {
return /* @__PURE__ */ jsx12("div", { className: "color-picker-content--default shades", children: shades.map((color, i) => /* @__PURE__ */ jsxs5(
"button",
{
ref: i === shade && activeColorPickerSection === "shades" ? btnRef : void 0,
tabIndex: -1,
type: "button",
className: clsx6(
"color-picker__button color-picker__button--large",
{ active: i === shade }
),
"aria-label": "Shade",
title: `${colorName} - ${i + 1}`,
style: color ? { "--swatch-color": color } : void 0,
onClick: () => {
onChange(color);
setActiveColorPickerSection("shades");
},
children: [
/* @__PURE__ */ jsx12("div", { className: "color-picker__button-outline" }),
/* @__PURE__ */ jsx12(HotkeyLabel_default, { color, keyLabel: i + 1, isShade: true })
]
},
i
)) });
}
}
return /* @__PURE__ */ jsxs5(
"div",
{
className: "color-picker-content--default",
style: { position: "relative" },
tabIndex: -1,
children: [
/* @__PURE__ */ jsx12(
"button",
{
type: "button",
tabIndex: -1,
className: "color-picker__button color-picker__button--large color-picker__button--no-focus-visible"
}
),
/* @__PURE__ */ jsx12(
"div",
{
tabIndex: -1,
style: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
textAlign: "center",
fontSize: "0.75rem"
},
children: t("colorPicker.noShades")
}
)
]
}
);
};
// components/ColorPicker/PickerColorList.tsx
import clsx7 from "clsx";
import { useEffect as useEffect5, useRef as useRef3 } from "react";
import { jsx as jsx13, jsxs as jsxs6 } from "react/jsx-runtime";
var PickerColorList = ({
palette: palette2,
color,
onChange,
label,
activeShade
}) => {
const colorObj = getColorNameAndShadeFromColor({
color: color || "transparent",
palette: palette2
});
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
activeColorPickerSectionAtom
);
const btnRef = useRef3(null);
useEffect5(() => {
if (btnRef.current && activeColorPickerSection === "baseColors") {
btnRef.current.focus();
}
}, [colorObj?.colorName, activeColorPickerSection]);
return /* @__PURE__ */ jsx13("div", { className: "color-picker-content--default", children: Object.entries(palette2).map(([key, value], index) => {
const color2 = (Array.isArray(value) ? value[activeShade] : value) || "transparent";
const keybinding = colorPickerHotkeyBindings[index];
const label2 = t(
`colors.${key.replace(/\d+/, "")}`,
null,
""
);
return /* @__PURE__ */ jsxs6(
"button",
{
ref: colorObj?.colorName === key ? btnRef : void 0,
tabIndex: -1,
type: "button",
className: clsx7(
"color-picker__button color-picker__button--large",
{
active: colorObj?.colorName === key,
"is-transparent": color2 === "transparent" || !color2
}
),
onClick: () => {
onChange(color2);
setActiveColorPickerSection("baseColors");
},
title: `${label2}${color2.startsWith("#") ? ` ${color2}` : ""} \u2014 ${keybinding}`,
"aria-label": `${label2} \u2014 ${keybinding}`,
style: color2 ? { "--swatch-color": color2 } : void 0,
"data-testid": `color-${key}`,
children: [
/* @__PURE__ */ jsx13("div", { className: "color-picker__button-outline" }),
/* @__PURE__ */ jsx13(HotkeyLabel_default, { color: color2, keyLabel: keybinding })
]
},
key
);
}) });
};
var PickerColorList_default = PickerColorList;
// components/ColorPicker/CustomColorList.tsx
import clsx8 from "clsx";
import { useEffect as useEffect6, useRef as useRef4 } from "react";
import { jsx as jsx14, jsxs as jsxs7 } from "react/jsx-runtime";
var CustomColorList = ({
colors,
color,
onChange,
label
}) => {
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
activeColorPickerSectionAtom
);
const btnRef = useRef4(null);
useEffect6(() => {
if (btnRef.current) {
btnRef.current.focus();
}
}, [color, activeColorPickerSection]);
return /* @__PURE__ */ jsx14("div", { className: "color-picker-content--default", children: colors.map((c, i) => {
return /* @__PURE__ */ jsxs7(
"button",
{
ref: color === c ? btnRef : void 0,
tabIndex: -1,
type: "button",
className: clsx8(
"color-picker__button color-picker__button--large",
{
active: color === c,
"is-transparent": c === "transparent" || !c
}
),
onClick: () => {
onChange(c);
setActiveColorPickerSection("custom");
},
title: c,
"aria-label": label,
style: { "--swatch-color": c },
children: [
/* @__PURE__ */ jsx14("div", { className: "color-picker__button-outline" }),
/* @__PURE__ */ jsx14(HotkeyLabel_default, { color: c, keyLabel: i + 1, isCustomColor: true })
]
},
i
);
}) });
};
// components/ColorPicker/keyboardNavHandlers.ts
var arrowHandler = (eventKey, currentIndex, length) => {
const rows = Math.ceil(length / COLORS_PER_ROW);
currentIndex = currentIndex ?? -1;
switch (eventKey) {
case "ArrowLeft": {
const prevIndex = currentIndex - 1;
return prevIndex < 0 ? length - 1 : prevIndex;
}
case "ArrowRight": {
return (currentIndex + 1) % length;
}
case "ArrowDown": {
const nextIndex = currentIndex + COLORS_PER_ROW;
return nextIndex >= length ? currentIndex % COLORS_PER_ROW : nextIndex;
}
case "ArrowUp": {
const prevIndex = currentIndex - COLORS_PER_ROW;
const newIndex = prevIndex < 0 ? COLORS_PER_ROW * rows + prevIndex : prevIndex;
return newIndex >= length ? void 0 : newIndex;
}
}
};
var hotkeyHandler = ({
e,
colorObj,
onChange,
palette: palette2,
customColors,
setActiveColorPickerSection,
activeShade
}) => {
if (colorObj?.shade != null) {
if (["Digit1", "Digit2", "Digit3", "Digit4", "Digit5"].includes(e.code) && e.shiftKey) {
const newShade = Number(e.code.slice(-1)) - 1;
onChange(palette2[colorObj.colorName][newShade]);
setActiveColorPickerSection("shades");
return true;
}
}
if (["1", "2", "3", "4", "5"].includes(e.key)) {
const c = customColors[Number(e.key) - 1];
if (c) {
onChange(customColors[Number(e.key) - 1]);
setActiveColorPickerSection("custom");
return true;
}
}
if (colorPickerHotkeyBindings.includes(e.key)) {
const index = colorPickerHotkeyBindings.indexOf(e.key);
const paletteKey = Object.keys(palette2)[index];
const paletteValue = palette2[paletteKey];
const r = Array.isArray(paletteValue) ? paletteValue[activeShade] : paletteValue;
onChange(r);
setActiveColorPickerSection("baseColors");
return true;
}
return false;
};
var colorPickerKeyNavHandler = ({
event,
activeColorPickerSection,
palette: palette2,
color,
onChange,
customColors,
setActiveColorPickerSection,
updateData,
activeShade,
onEyeDropperToggle,
onEscape
}) => {
if (event[KEYS.CTRL_OR_CMD]) {
return false;
}
if (event.key === KEYS.ESCAPE) {
onEscape(event);
return true;
}
if (event.key === KEYS.ALT) {
onEyeDropperToggle(true);
return true;
}
if (event.key === KEYS.I) {
onEyeDropperToggle();
return true;
}
const colorObj = getColorNameAndShadeFromColor({ color, palette: palette2 });
if (event.key === KEYS.TAB) {
const sectionsMap = {
custom: !!customColors.length,
baseColors: true,
shades: colorObj?.shade != null,
hex: true
};
const sections = Object.entries(sectionsMap).reduce((acc, [key, value]) => {
if (value) {
acc.push(key);
}
return acc;
}, []);
const activeSectionIndex = sections.indexOf(activeColorPickerSection);
const indexOffset = event.shiftKey ? -1 : 1;
const nextSectionIndex = activeSectionIndex + indexOffset > sections.length - 1 ? 0 : activeSectionIndex + indexOffset < 0 ? sections.length - 1 : activeSectionIndex + indexOffset;
const nextSection = sections[nextSectionIndex];
if (nextSection) {
setActiveColorPickerSection(nextSection);
}
if (nextSection === "custom") {
onChange(customColors[0]);
} else if (nextSection === "baseColors") {
const baseColorName = Object.entries(palette2).find(([name, shades]) => {
if (Array.isArray(shades)) {
return shades.includes(color);
} else if (shades === color) {
return name;
}
return null;
});
if (!baseColorName) {
onChange(COLOR_PALETTE.black);
}
}
event.preventDefault();
event.stopPropagation();
return true;
}
if (hotkeyHandler({
e: event,
colorObj,
onChange,
palette: palette2,
customColors,
setActiveColorPickerSection,
activeShade
})) {
return true;
}
if (activeColorPickerSection === "shades") {
if (colorObj) {
const { shade } = colorObj;
const newShade = arrowHandler(event.key, shade, COLORS_PER_ROW);
if (newShade !== void 0) {
onChange(palette2[colorObj.colorName][newShade]);
return true;
}
}
}
if (activeColorPickerSection === "baseColors") {
if (colorObj) {
const { colorName } = colorObj;
const colorNames = Object.keys(palette2);
const indexOfColorName = colorNames.indexOf(colorName);
const newColorIndex = arrowHandler(
event.key,
indexOfColorName,
colorNames.length
);
if (newColorIndex !== void 0) {
const newColorName = colorNames[newColorIndex];
const newColorNameValue = palette2[newColorName];
onChange(
Array.isArray(newColorNameValue) ? newColorNameValue[activeShade] : newColorNameValue
);
return true;
}
}
}
if (activeColorPickerSection === "custom") {
const indexOfColor = customColors.indexOf(color);
const newColorIndex = arrowHandler(
event.key,
indexOfColor,
customColors.length
);
if (newColorIndex !== void 0) {
const newColor = customColors[newColorIndex];
onChange(newColor);
return true;
}
}
return false;
};
// components/ColorPicker/PickerHeading.tsx
import { jsx as jsx15 } from "react/jsx-runtime";
var PickerHeading = ({ children }) => /* @__PURE__ */ jsx15("div", { className: "color-picker__heading", children });
var PickerHeading_default = PickerHeading;
// components/ColorPicker/Picker.tsx
import { jsx as jsx16, jsxs as jsxs8 } from "react/jsx-runtime";
var Picker = ({
color,
onChange,
label,
type,
elements,
palette: palette2,
updateData,
children,
onEyeDropperToggle,
onEscape
}) => {
const [customColors] = React4.useState(() => {
if (type === "canvasBackground") {
return [];
}
return getMostUsedCustomColors(elements, type, palette2);
});
const [activeColorPickerSection, setActiveColorPickerSection] = useAtom(
activeColorPickerSectionAtom
);
const colorObj = getColorNameAndShadeFromColor({
color,
palette: palette2
});
useEffect7(() => {
if (!activeColorPickerSection) {
const isCustom = isCustomColor({ color, palette: palette2 });
const isCustomButNotInList = isCustom && !customColors.includes(color);
setActiveColorPickerSection(
isCustomButNotInList ? "hex" : isCustom ? "custom" : colorObj?.shade != null ? "shades" : "baseColors"
);
}
}, [
activeColorPickerSection,
color,
palette2,
setActiveColorPickerSection,
colorObj,
customColors
]);
const [activeShade, setActiveShade] = useState4(
colorObj?.shade ?? (type === "elementBackground" ? DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX : DEFAULT_ELEMENT_STROKE_COLOR_INDEX)
);
useEffect7(() => {
if (colorObj?.shade != null) {
setActiveShade(colorObj.shade);
}
const keyup = (event) => {
if (event.key === KEYS.ALT) {
onEyeDropperToggle(false);
}
};
document.addEventListener("keyup" /* KEYUP */, keyup, { capture: true });
return () => {
document.removeEventListener("keyup" /* KEYUP */, keyup, { capture: true });
};
}, [colorObj, onEyeDropperToggle]);
const pickerRef = React4.useRef(null);
return /* @__PURE__ */ jsx16("div", { role: "dialog", "aria-modal": "true", "aria-label": t("labels.colorPicker"), children: /* @__PURE__ */ jsxs8(
"div",
{
ref: pickerRef,
onKeyDown: (event) => {
const handled = colorPickerKeyNavHandler({
event,
activeColorPickerSection,
palette: palette2,
color,
onChange,
onEyeDropperToggle,
customColors,
setActiveColorPickerSection,
updateData,
activeShade,
onEscape
});
if (handled) {
event.preventDefault();
event.stopPropagation();
}
},
className: "color-picker-content properties-content",
tabIndex: -1,
children: [
!!customColors.length && /* @__PURE__ */ jsxs8("div", { children: [
/* @__PURE__ */ jsx16(PickerHeading_default, { children: t("colorPicker.mostUsedCustomColors") }),
/* @__PURE__ */ jsx16(
CustomColorList,
{
colors: customColors,
color,
label: t("colorPicker.mostUsedCustomColors"),
onChange
}
)
] }),
/* @__PURE__ */ jsxs8("div", { children: [
/* @__PURE__ */ jsx16(PickerHeading_default, { children: t("colorPicker.colors") }),
/* @__PURE__ */ jsx16(
PickerColorList_default,
{
color,
label,
palette: palette2,
onChange,
activeShade
}
)
] }),
/* @__PURE__ */ jsxs8("div", { children: [
/* @__PURE__ */ jsx16(PickerHeading_default, { children: t("colorPicker.shades") }),
/* @__PURE__ */ jsx16(ShadeList, { hex: color, onChange, palette: palette2 })
] }),
children
]
}
) });
};
// components/ColorPicker/ColorPicker.tsx
import * as Popover2 from "@radix-ui/react-popover";
import clsx12 from "clsx";
import { useRef as useRef8 } from "react";
// components/ColorPicker/ColorInput.tsx
import { useCallback, useEffect as useEffect10, useRef as useRef7, useState as useState6 } from "react";
// components/EyeDropper.tsx
import { useEffect as useEffect9, useRef as useRef6 } from "react";
import { createPortal } from "react-dom";
// context/ui-appState.ts
import React5 from "react";
var UIAppStateContext = React5.createContext(null);
var useUIAppState = () => React5.useContext(UIAppStateContext);
// hooks/useCreatePortalContainer.ts
import { useState as useState5, useLayoutEffect } from "react";
var useCreatePortalContainer = (opts) => {
const [div, setDiv] = useState5(null);
const device = useDevice();
const { theme } = useUIAppState();
const { container: excalidrawContainer } = useExcalidrawContainer();
useLayoutEffect(() => {
if (div) {
div.className = "";
div.classList.add("excalidraw", ...opts?.className?.split(/\s+/) || []);
div.classList.toggle("excalidraw--mobile", device.editor.isMobile);
div.classList.toggle("theme--dark", theme === THEME.DARK);
}
}, [div, theme, device.editor.isMobile, opts?.className]);
useLayoutEffect(() => {
const container = opts?.parentSelector ? excalidrawContainer?.querySelector(opts.parentSelector) : document.body;
if (!container) {
return;
}
const div2 = document.createElement("div");
container.appendChild(div2);
setDiv(div2);
return () => {
container.removeChild(div2);
};
}, [excalidrawContainer, opts?.parentSelector]);
return div;
};
// hooks/useOutsideClick.ts
import { useEffect as useEffect8 } from "react";
function useOutsideClick(ref, callback, isInside) {
useEffect8(() => {
function onOutsideClick(event) {
const _event = event;
if (!ref.current) {
return;
}
const isInsideOverride = isInside?.(_event, ref.current);
if (isInsideOverride === true) {
return;
} else if (isInsideOverride === false) {
return callback(_event);
}
if (ref.current.contains(_event.target) || // target is detached from DOM (happens when the element is removed
// on a pointerup event fired *before* this handler's pointerup is
// dispatched)
!document.documentElement.contains(_event.target)) {
return;
}
const isClickOnRadixPortal = _event.target.closest("[data-radix-portal]") || // when radix popup is in "modal" mode, it disables pointer events on
// the `body` element, so the target element is going to be the `html`
// (note: this won't work if we selectively re-enable pointer events on
// specific elements as we do with navbar or excalidraw UI elements)
_event.target === document.documentElement && document.body.style.pointerEvents === "none";
if (isClickOnRadixPortal) {
return;
}
if (_event.target.closest("[data-prevent-outside-click]")) {
return;
}
callback(_event);
}
document.addEventListener("pointerdown" /* POINTER_DOWN */, onOutsideClick);
document.addEventListener("touchstart" /* TOUCH_START */, onOutsideClick);
return () => {
document.removeEventListener("pointerdown" /* POINTER_DOWN */, onOutsideClick);
document.removeEventListener("touchstart" /* TOUCH_START */, onOutsideClick);
};
}, [ref, callback, isInside]);
}
// hooks/useStable.ts
import { useRef as useRef5 } from "react";
var useStable = (value) => {
const ref = useRef5(value);
Object.assign(ref.current, value);
return ref.current;
};
// components/EyeDropper.tsx
import { jsx as jsx17 } from "react/jsx-runtime";
var activeEyeDropperAtom = atom(null);
var EyeDropper = ({ onCancel, onChange, onSelect, colorPickerType }) => {
const eyeDropperContainer = useCreatePortalContainer({
className: "excalidraw-eye-dropper-backdrop",
parentSelector: ".excalidraw-eye-dropper-container"
});
const appState = useUIAppState();
const elements = useExcalidrawElements();
const app = useApp();
const selectedElements = getSelectedElements(elements, appState);
const stableProps = useStable({
app,
onCancel,
onChange,
onSelect,
selectedElements
});
const { container: excalidrawContainer } = useExcalidrawContainer();
useEffect9(() => {
const colorPreviewDiv = ref.current;
if (!colorPreviewDiv || !app.canvas || !eyeDropperContainer) {
return;
}
let isHoldingPointerDown = false;
const ctx = app.canvas.getContext("2d");
const getCurrentColor = ({
clientX,
clientY
}) => {
const pixel = ctx.getImageData(
(clientX - appState.offsetLeft) * window.devicePixelRatio,
(clientY - appState.offsetTop) * window.devicePixelRatio,
1,
1
).data;
return rgbToHex(pixel[0], pixel[1], pixel[2]);
};
const mouseMoveListener = ({
clientX,
clientY,
altKey
}) => {
colorPreviewDiv.style.top = `${clientY + 20}px`;
colorPreviewDiv.style.left = `${clientX + 20}px`;
const currentColor = getCurrentColor({ clientX, clientY });
if (isHoldingPointerDown) {
stableProps.onChange(
colorPickerType,
currentColor,
stableProps.selectedElements,
{ altKey }
);
}
colorPreviewDiv.style.background = currentColor;
};
const onCancel2 = () => {
stableProps.onCancel();
};
const onSelect2 = (color, event) => {
stableProps.onSelect(color, event);
};
const pointerDownListener = (event) => {
isHoldingPointerDown = true;
event.stopImmediatePropagation();
};
const pointerUpListener = (event) => {
isHoldingPointerDown = false;
excalidrawContainer?.focus();
event.stopImmediatePropagation();
event.preventDefault();
onSelect2(getCurrentColor(event), event);
};
const keyDownListener = (event) => {
if (event.key === KEYS.ESCAPE) {
event.preventDefault();
event.stopImmediatePropagation();
onCancel2();
}
};
eyeDropperContainer.tabIndex = -1;
eyeDropperContainer.focus();
mouseMoveListener({
clientX: stableProps.app.lastViewportPosition.x,
clientY: stableProps.app.lastViewportPosition.y,
altKey: false
});
eyeDropperContainer.addEventListener("keydown" /* KEYDOWN */, keyDownListener);
eyeDropperContainer.addEventListener(
"pointerdown" /* POINTER_DOWN */,
pointerDownListener
);
eyeDropperContainer.addEventListener("pointerup" /* POINTER_UP */, pointerUpListener);
window.addEventListener("pointermove", mouseMoveListener, {
passive: true
});
window.addEventListener("blur" /* BLUR */, onCancel2);
return () => {
isHoldingPointerDown = false;
eyeDropperContainer.removeEventListener("keydown" /* KEYDOWN */, keyDownListener);
eyeDropperContainer.removeEventListener(
"pointerdown" /* POINTER_DOWN */,
pointerDownListener
);
eyeDropperContainer.removeEventListener(
"pointerup" /* POINTER_UP */,
pointerUpListener
);
window.removeEventListener("pointermove", mouseMoveListener);
window.removeEventListener("blur" /* BLUR */, onCancel2);
};
}, [
stableProps,
app.canvas,
eyeDropperContainer,
colorPickerType,
excalidrawContainer,
appState.offsetLeft,
appState.offsetTop
]);
const ref = useRef6(null);
useOutsideClick(
ref,
() => {
onCancel();
},
(event) => {
if (event.target.closest(
".excalidraw-eye-dropper-trigger, .excalidraw-eye-dropper-backdrop"
)) {
return true;
}
return false;
}
);
if (!eyeDropperContainer) {
return null;
}
return createPortal(
/* @__PURE__ */ jsx17("div", { ref, className: "excalidraw-eye-dropper-preview" }),
eyeDropperContainer
);
};
// components/ColorPicker/ColorInput.tsx
import clsx9 from "clsx";
import { Fragment, jsx as jsx18, jsxs as jsxs9 } from "react/jsx-runtime";
var ColorInput = ({
color,
onChange,
label,
colorPickerType
}) => {
const device = useDevice();
const [innerValue, setInnerValue] = useState6(color);
const [activeSection, setActiveColorPickerSection] = useAtom(
activeColorPickerSectionAtom
);
useEffect10(() => {
setInnerValue(color);
}, [color]);
const changeColor = useCallback(
(inputValue) => {
const value = inputValue.toLowerCase();
const color2 = getColor(value);
if (color2) {
onChange(color2);
}
setInnerValue(value);
},
[onChange]
);
const inputRef = useRef7(null);
const eyeDropperTriggerRef = useRef7(null);
useEffect10(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, [activeSection]);
const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
useEffect10(() => {
return () => {
setEyeDropperState(null);
};
}, [setEyeDropperState]);
return /* @__PURE__ */ jsxs9("div", { className: "color-picker__input-label", children: [
/* @__PURE__ */ jsx18("div", { className: "color-picker__input-hash", children: "#" }),
/* @__PURE__ */ jsx18(
"input",
{
ref: activeSection === "hex" ? inputRef : void 0,
style: { border: 0, padding: 0 },
spellCheck: false,
className: "color-picker-input",
"aria-label": label,
onChange: (event) => {
changeColor(event.target.value);
},
value: (innerValue || "").replace(/^#/, ""),
onBlur: () => {
setInnerValue(color);
},
tabIndex: -1,
onFocus: () => setActiveColorPickerSection("hex"),
onKeyDown: (event) => {
if (event.key === KEYS.TAB) {
return;
} else if (event.key === KEYS.ESCAPE) {
eyeDropperTriggerRef.current?.focus();
}
event.stopPropagation();
}
}
),
!device.editor.isMobile && /* @__PURE__ */ jsxs9(Fragment, { children: [
/* @__PURE__ */ jsx18(
"div",
{
style: {
width: "1px",
height: "1.25rem",
backgroundColor: "var(--default-border-color)"
}
}
),
/* @__PURE__ */ jsx18(
"div",
{
ref: eyeDropperTriggerRef,
className: clsx9("excalidraw-eye-dropper-trigger", {
selected: eyeDropperState
}),
onClick: () => setEyeDropperState(
(s) => s ? null : {
keepOpenOnAlt: false,
onSelect: (color2) => onChange(color2),
colorPickerType
}
),
title: `${t(
"labels.eyeDropper"
)} \u2014 ${KEYS.I.toLocaleUpperCase()} or ${getShortcutKey("Alt")} `,
children: eyeDropperIcon
}
)
] })
] });
};
// components/PropertiesPopover.tsx
import React7 from "react";
import clsx11 from "clsx";
import * as Popover from "@radix-ui/react-popover";
// components/Island.tsx
import React6 from "react";
import clsx10 from "clsx";
import { jsx as jsx19 } from "react/jsx-runtime";
var Island = React6.forwardRef(
({ children, padding, className, style }, ref) => /* @__PURE__ */ jsx19(
"div",
{
className: clsx10("Island", className),
style: { "--padding": padding, ...style },
ref,
children
}
)
);
// components/PropertiesPopover.tsx
import { jsx as jsx20, jsxs as jsxs10 } from "react/jsx-runtime";
var PropertiesPopover = React7.forwardRef(
({
className,
container,
children,
style,
onClose,
onKeyDown,
onFocusOutside,
onPointerLeave,
onPointerDownOutside
}, ref) => {
const device = useDevice();
return /* @__PURE__ */ jsx20(Popover.Portal, { container, children: /* @__PURE__ */ jsxs10(
Popover.Content,
{
ref,
className: clsx11("focus-visible-none", className),
"data-prevent-outside-click": true,
side: device.editor.isMobile && !device.viewport.isLandscape ? "bottom" : "right",
align: device.editor.isMobile && !device.viewport.isLandscape ? "center" : "start",
alignOffset: -16,
sideOffset: 20,
style: {
zIndex: "var(--zIndex-popup)"
},
onPointerLeave,
onKeyDown,
onFocusOutside,
onPointerDownOutside,
onCloseAutoFocus: (e) => {
e.stopPropagation();
e.preventDefault();
if (container && !isInteractive(document.activeElement)) {
container.focus();
}
onClose();
},
children: [
/* @__PURE__ */ jsx20(Island, { padding: 3, style, children }),
/* @__PURE__ */ jsx20(
Popover.Arrow,
{
width: 20,
height: 10,
style: {
fill: "var(--popup-bg-color)",
filter: "drop-shadow(rgba(0, 0, 0, 0.05) 0px 3px 2px)"
}
}
)
]
}
) });
}
);
// components/ColorPicker/ColorPicker.tsx
import { jsx as jsx21, jsxs as jsxs11 } from "react/jsx-runtime";
var isValidColor = (color) => {
const style = new Option().style;
style.color = color;
return !!style.color;
};
var getColor = (color) => {
if (isTransparent(color)) {
return color;
}
return isValidColor(`#${color}`) ? `#${color}` : isValidColor(color) ? color : null;
};
var ColorPickerPopupContent = ({
type,
color,
onChange,
label,
elements,
palette: palette2 = COLOR_PALETTE,
updateData
}) => {
const { container } = useExcalidrawContainer();
const [, setActiveColorPickerSection] = useAtom(activeColorPickerSectionAtom);
const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
const colorInputJSX = /* @__PURE__ */ jsxs11("div", { children: [
/* @__PURE__ */ jsx21(PickerHeading_default, { children: t("colorPicker.hexCode") }),
/* @__PURE__ */ jsx21(
ColorInput,
{
color,
label,
onChange: (color2) => {
onChange(color2);
},
colorPickerType: type
}
)
] });
const popoverRef = useRef8(null);
const focusPickerContent = () => {
popoverRef.current?.querySelector(".color-picker-content")?.focus();
};
return /* @__PURE__ */ jsx21(
PropertiesPopover,
{
container,
style: { maxWidth: "13rem" },
onFocusOutside: (event) => {
focusPickerContent();
event.preventDefault();
},
onPointerDownOutside: (event) => {
if (eyeDropperState) {
event.preventDefault();
}
},
onClose: () => {
updateData({ openPopup: null });
setActiveColorPickerSection(null);
},
children: palette2 ? /* @__PURE__ */ jsx21(
Picker,
{
palette: palette2,
color,
onChange: (changedColor) => {
onChange(changedColor);
},
onEyeDropperToggle: (force) => {
setEyeDropperState((state) => {
if (force) {
state = state || {
keepOpenOnAlt: true,
onSelect: onChange,
colorPickerType: type
};
state.keepOpenOnAlt = true;
return state;
}
return force === false || state ? null : {
keepOpenOnAlt: false,
onSelect: onChange,
colorPickerType: type
};
});
},
onEscape: (event) => {
if (eyeDropperState) {
setEyeDropperState(null);
} else {
updateData({ openPopup: null });
}
},
label,
type,
elements,
updateData,
children: colorInputJSX
}
) : colorInputJSX
}
);
};
var ColorPickerTrigger = ({
label,
color,
type
}) => {
return /* @__PURE__ */ jsx21(
Popover2.Trigger,
{
type: "button",
className: clsx12("color-picker__button active-color properties-trigger", {
"is-transparent": color === "transparent" || !color
}),
"aria-label": label,
style: color ? { "--swatch-color": color } : void 0,
title: type === "elementStroke" ? t("labels.showStroke") : t("labels.showBackground"),
children: /* @__PURE__ */ jsx21("div", { className: "color-picker__button-outline" })
}
);
};
var ColorPicker = ({
type,
color,
onChange,
label,
elements,
palette: palette2 = COLOR_PALETTE,
topPicks,
updateData,
appState
}) => {
return /* @__PURE__ */ jsx21("div", { children: /* @__PURE__ */ jsxs11("div", { role: "dialog", "aria-modal": "true", className: "color-picker-container", children: [
/* @__PURE__ */ jsx21(
TopPicks,
{
activeColor: color,
onChange,
type,
topPicks
}
),
/* @__PURE__ */ jsx21(ButtonSeparator, {}),
/* @__PURE__ */ jsxs11(
Popover2.Root,
{
open: appState.openPopup === type,
onOpenChange: (open) => {
updateData({ openPopup: open ? type : null });
},
children: [
/* @__PURE__ */ jsx21(ColorPickerTrigger, { color, label, type }),
appState.openPopup === type && /* @__PURE__ */ jsx21(
ColorPickerPopupContent,
{
type,
color,
onChange,
label,
elements,
palette: palette2,
updateData
}
)
]
}
)
] }) });
};
// components/IconPicker.tsx
import React8, { useEffect as useEffect11 } from "react";
import * as Popover3 from "@radix-ui/react-popover";
import clsx13 from "clsx";
// components/InlineIcon.tsx
import { jsx as jsx22 } from "react/jsx-runtime";
var InlineIcon = ({ icon }) => {
return /* @__PURE__ */ jsx22(
"span",
{
style: {
width: "1em",
margin: "0 0.5ex 0 0.5ex",
display: "inline-block",
lineHeight: 0,
verticalAlign: "middle"
},
children: icon
}
);
};
// components/Stats/Collapsible.tsx
import { Fragment as Fragment2, jsx as jsx23, jsxs as jsxs12 } from "react/jsx-runtime";
var Collapsible = ({
label,
open,
openTrigger,
children,
className
}) => {
return /* @__PURE__ */ jsxs12(Fragment2, { children: [
/* @__PURE__ */ jsxs12(
"div",
{
style: {
cursor: "pointer",
display: "flex",
justifyContent: "space-between",
alignItems: "center"
},
className,
onClick: openTrigger,
children: [
label,
/* @__PURE__ */ jsx23(InlineIcon, { icon: open ? collapseUpIcon : collapseDownIcon })
]
}
),
open && /* @__PURE__ */ jsx23("div", { style: { display: "flex", flexDirection: "column" }, children })
] });
};
var Collapsible_default = Collapsible;
// components/IconPicker.tsx
import { jsx as jsx24, jsxs as jsxs13 } from "react/jsx-runtime";
var moreOptionsAtom = atom(false);
function Picker2({
options,
value,
label,
onChange,
onClose,
numberOfOptionsToAlwaysShow = options.length
}) {
const device = useDevice();
const handleKeyDown = (event) => {
const pressedOption = options.find(
(option) => option.keyBinding === event.key.toLowerCase()
);
if (!(event.metaKey || event.altKey || event.ctrlKey) && pressedOption) {
onChange(pressedOption.value);
event.preventDefault();
} else if (event.key === KEYS.TAB) {
const index = options.findIndex((option) => option.value === value);
const nextIndex = event.shiftKey ? (options.length + index - 1) % options.length : (index + 1) % options.length;
onChange(options[nextIndex].value);
} else if (isArrowKey(event.key)) {
const isRTL = getLanguage().rtl;
const index = options.findIndex((option) => option.value === value);
if (index !== -1) {
const length = options.length;
let nextIndex = index;
switch (event.key) {
case (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT):
nextIndex = (index + 1) % length;
break;
case (isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT):
nextIndex = (length + index - 1) % length;
break;
case KEYS.ARROW_DOWN: {
nextIndex = (index + (numberOfOptionsToAlwaysShow ?? 1)) % length;
break;
}
case KEYS.ARROW_UP: {
nextIndex = (length + index - (numberOfOptionsToAlwaysShow ?? 1)) % length;
break;
}
}
onChange(options[nextIndex].value);
}
event.preventDefault();
} else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
event.preventDefault();
onClose();
}
event.nativeEvent.stopImmediatePropagation();
event.stopPropagation();
};
const [showMoreOptions, setShowMoreOptions] = useAtom(moreOptionsAtom);
const alwaysVisibleOptions = React8.useMemo(
() => options.slice(0, numberOfOptionsToAlwaysShow),
[options, numberOfOptionsToAlwaysShow]
);
const moreOptions = React8.useMemo(
() => options.slice(numberOfOptionsToAlwaysShow),
[options, numberOfOptionsToAlwaysShow]
);
useEffect11(() => {
if (!alwaysVisibleOptions.some((option) => option.value === value)) {
setShowMoreOptions(true);
}
}, [value, alwaysVisibleOptions, setShowMoreOptions]);
const renderOptions = (options2) => {
return /* @__PURE__ */ jsx24("div", { className: "picker-content", children: options2.map((option, i) => /* @__PURE__ */ jsxs13(
"button",
{
type: "button",
className: clsx13("picker-option", {
active: value === option.value
}),
onClick: (event) => {
onChange(option.value);
},
title: `${option.text} ${option.keyBinding && `\u2014 ${option.keyBinding.toUpperCase()}`}`,
"aria-label": option.text || "none",
"aria-keyshortcuts": option.keyBinding || void 0,
ref: (ref) => {
if (value === option.value) {
setTimeout(() => {
ref?.focus();
}, 0);
}
},
children: [
option.icon,
option.keyBinding && /* @__PURE__ */ jsx24("span", { className: "picker-keybinding", children: option.keyBinding })
]
},
option.text
)) });
};
return /* @__PURE__ */ jsx24(
Popover3.Content,
{
side: device.editor.isMobile && !device.viewport.isLandscape ? "top" : "bottom",
align: "start",
sideOffset: 12,
style: { zIndex: "var(--zIndex-popup)" },
onKeyDown: handleKeyDown,
children: /* @__PURE__ */ jsxs13(
"div",
{
className: `picker`,
role: "dialog",
"aria-modal": "true",
"aria-label": label,
children: [
renderOptions(alwaysVisibleOptions),
moreOptions.length > 0 && /* @__PURE__ */ jsx24(
Collapsible_default,
{
label: t("labels.more_options"),
open: showMoreOptions,
openTrigger: () => {
setShowMoreOptions((value2) => !value2);
},
className: "picker-collapsible",
children: renderOptions(moreOptions)
}
)
]
}
)
}
);
}
function IconPicker({
value,
label,
options,
onChange,
group = "",
numberOfOptionsToAlwaysShow
}) {
const [isActive, setActive] = React8.useState(false);
const rPickerButton = React8.useRef(null);
return /* @__PURE__ */ jsx24("div", { children: /* @__PURE__ */ jsxs13(Popover3.Root, { open: isActive, onOpenChange: (open) => setActive(open), children: [
/* @__PURE__ */ jsx24(
Popover3.Trigger,
{
name: group,
type: "button",
"aria-label": label,
onClick: () => setActive(!isActive),
ref: rPickerButton,
className: isActive ? "active" : "",
children: options.find((option) => option.value === value)?.icon
}
),
isActive && /* @__PURE__ */ jsx24(
Picker2,
{
options,
value,
label,
onChange,
onClose: () => {
setActive(false);
},
numberOfOptionsToAlwaysShow
}
)
] }) });
}
// components/FontPicker/FontPicker.tsx
import React13, { useCallback as useCallback3, useMemo as useMemo3 } from "react";
import * as Popover5 from "@radix-ui/react-popover";
// components/FontPicker/FontPickerList.tsx
import React12, {
useMemo,
useState as useState7,
useRef as useRef10,
useEffect as useEffect13,
useCallback as useCallback2
} from "react";
// components/QuickSearch.tsx
import clsx14 from "clsx";
import React9 from "react";
import { jsx as jsx25, jsxs as jsxs14 } from "react/jsx-runtime";
var QuickSearch = React9.forwardRef(
({ className, placeholder, onChange }, ref) => {
return /* @__PURE__ */ jsxs14("div", { className: clsx14("QuickSearch__wrapper", className), children: [
searchIcon,
/* @__PURE__ */ jsx25(
"input",
{
ref,
className: "QuickSearch__input",
type: "text",
placeholder,
onChange: (e) => onChange(e.target.value.trim().toLowerCase())
}
)
] });
}
);
// components/ScrollableList.tsx
import clsx15 from "clsx";
import { Children } from "react";
import { jsx as jsx26 } from "react/jsx-runtime";
var ScrollableList = ({
className,
placeholder,
children
}) => {
const isEmpty = !Children.count(children);
return /* @__PURE__ */ jsx26("div", { className: clsx15("ScrollableList__wrapper", className), role: "menu", children: isEmpty ? /* @__PURE__ */ jsx26("div", { className: "empty", children: placeholder }) : children });
};
// components/dropdownMenu/DropdownMenuGroup.tsx
import { jsx as jsx27, jsxs as jsxs15 } from "react/jsx-runtime";
var MenuGroup = ({
children,
className = "",
style,
title
}) => {
return /* @__PURE__ */ jsxs15("div", { className: `dropdown-menu-group ${className}`, style, children: [
title && /* @__PURE__ */ jsx27("p", { className: "dropdown-menu-group-title", children: title }),
children
] });
};
var DropdownMenuGroup_default = MenuGroup;
MenuGroup.displayName = "DropdownMenuGroup";
// components/dropdownMenu/DropdownMenuItem.tsx
import { useEffect as useEffect12, useRef as useRef9 } from "react";
// components/dropdownMenu/common.ts
import React10, { useContext } from "react";
var DropdownMenuContentPropsContext = React10.createContext({});
var getDropdownMenuItemClassName = (className = "", selected = false, hovered = false) => {
return `dropdown-menu-item dropdown-menu-item-base ${className}
${selected ? "dropdown-menu-item--selected" : ""} ${hovered ? "dropdown-menu-item--hovered" : ""}`.trim();
};
var useHandleDropdownMenuItemClick = (origOnClick, onSelect) => {
const DropdownMenuContentProps = useContext(DropdownMenuContentPropsContext);
return composeEventHandlers(origOnClick, (event) => {
const itemSelectEvent = new CustomEvent("menu.itemSelect" /* MENU_ITEM_SELECT */, {
bubbles: true,
cancelable: true
});
onSelect?.(itemSelectEvent);
if (!itemSelectEvent.defaultPrevented) {
DropdownMenuContentProps.onSelect?.(itemSelectEvent);
}
});
};
// components/dropdownMenu/DropdownMenuItemContent.tsx
import { Fragment as Fragment3, jsx as jsx28, jsxs as jsxs16 } from "react/jsx-runtime";
var MenuItemContent = ({
textStyle,
icon,
shortcut,
children
}) => {
const device = useDevice();
return /* @__PURE__ */ jsxs16(Fragment3, { children: [
icon && /* @__PURE__ */ jsx28("div", { className: "dropdown-menu-item__icon", children: icon }),
/* @__PURE__ */ jsx28("div", { style: textStyle, className: "dropdown-menu-item__text", children }),
shortcut && !device.editor.isMobile && /* @__PURE__ */ jsx28("div", { className: "dropdown-menu-item__shortcut", children: shortcut })
] });
};
var DropdownMenuItemContent_default = MenuItemContent;
// components/dropdownMenu/DropdownMenuItem.tsx
import { jsx as jsx29 } from "react/jsx-runtime";
var DropdownMenuItem = ({
icon,
value,
order,
children,
shortcut,
className,
hovered,
selected,
textStyle,
onSelect,
onClick,
...rest
}) => {
const handleClick = useHandleDropdownMenuItemClick(onClick, onSelect);
const ref = useRef9(null);
useEffect12(() => {
if (hovered) {
if (order === 0) {
ref.current?.scrollIntoView({ block: "end" });
} else {
ref.current?.scrollIntoView({ block: "nearest" });
}
}
}, [hovered, order]);
return /* @__PURE__ */ jsx29(
"button",
{
...rest,
ref,
value,
onClick: handleClick,
className: getDropdownMenuItemClassName(className, selected, hovered),
title: rest.title ?? rest["aria-label"],
children: /* @__PURE__ */ jsx29(DropdownMenuItemContent_default, { textStyle, icon, shortcut, children })
}
);
};
DropdownMenuItem.displayName = "DropdownMenuItem";
var DropDownMenuItemBadgeType = {
GREEN: "green",
RED: "red",
BLUE: "blue"
};
var DropDownMenuItemBadge = ({
type = DropDownMenuItemBadgeType.BLUE,
children
}) => {
const { theme } = useExcalidrawAppState();
const style = {
display: "inline-flex",
marginLeft: "auto",
padding: "2px 4px",
borderRadius: 6,
fontSize: 9,
fontFamily: "Cascadia, monospace",
border: theme === THEME.LIGHT ? "1.5px solid white" : "none"
};
switch (type) {
case DropDownMenuItemBadgeType.GREEN:
Object.assign(style, {
backgroundColor: "var(--background-color-badge)",
color: "var(--color-badge)"
});
break;
case DropDownMenuItemBadgeType.RED:
Object.assign(style, {
backgroundColor: "pink",
color: "darkred"
});
break;
case DropDownMenuItemBadgeType.BLUE:
default:
Object.assign(style, {
background: "var(--color-promo)",
color: "var(--color-surface-lowest)"
});
}
return /* @__PURE__ */ jsx29("div", { className: "DropDownMenuItemBadge", style, children });
};
DropDownMenuItemBadge.displayName = "DropdownMenuItemBadge";
DropdownMenuItem.Badge = DropDownMenuItemBadge;
var DropdownMenuItem_default = DropdownMenuItem;
// components/FontPicker/keyboardNavHandlers.ts
var fontPickerKeyHandler = ({
event,
inputRef,
hoveredFont,
filteredFonts,
onClose,
onSelect,
onHover
}) => {
if (!event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key.toLowerCase() === KEYS.F) {
inputRef.current?.focus();
return true;
}
if (event.key === KEYS.ESCAPE) {
onClose();
return true;
}
if (event.key === KEYS.ENTER) {
if (hoveredFont?.value) {
onSelect(hoveredFont.value);
}
return true;
}
if (event.key === KEYS.ARROW_DOWN) {
if (hoveredFont?.next) {
onHover(hoveredFont.next.value);
} else if (filteredFonts[0]?.value) {
onHover(filteredFonts[0].value);
}
return true;
}
if (event.key === KEYS.ARROW_UP) {
if (hoveredFont?.prev) {
onHover(hoveredFont.prev.value);
} else if (filteredFonts[filteredFonts.length - 1]?.value) {
onHover(filteredFonts[filteredFonts.length - 1].value);
}
return true;
}
};
// components/FontPicker/FontPickerList.tsx
import { jsx as jsx30, jsxs as jsxs17 } from "react/jsx-runtime";
var FontPickerList = React12.memo(
({
selectedFontFamily,
hoveredFontFamily,
onSelect,
onHover,
onLeave,
onOpen,
onClose
}) => {
const { container } = useExcalidrawContainer();
const { fonts } = useApp();
const { showDeprecatedFonts } = useAppProps();
const [searchTerm, setSearchTerm] = useState7("");
const inputRef = useRef10(null);
const allFonts = useMemo(
() => Array.from(Fonts.registered.entries()).filter(
([_, { metadata }]) => !metadata.serverSide && !metadata.fallback
).map(([familyId, { metadata, fontFaces }]) => {
const fontDescriptor = {
value: familyId,
icon: metadata.icon ?? FontFamilyNormalIcon,
text: fontFaces[0]?.fontFace?.family ?? "Unknown"
};
if (metadata.deprecated) {
Object.assign(fontDescriptor, {
deprecated: metadata.deprecated,
badge: {
type: DropDownMenuItemBadgeType.RED,
placeholder: t("fontList.badge.old")
}
});
}
return fontDescriptor;
}).sort(
(a, b) => a.text.toLowerCase() > b.text.toLowerCase() ? 1 : -1
),
[]
);
const sceneFamilies = useMemo(
() => new Set(fonts.getSceneFamilies()),
// cache per selected font family, so hover re-render won't mess it up
// eslint-disable-next-line react-hooks/exhaustive-deps
[selectedFontFamily]
);
const sceneFonts = useMemo(
() => allFonts.filter((font) => sceneFamilies.has(font.value)),
// always show all the fonts in the scene, even those that were deprecated
[allFonts, sceneFamilies]
);
const availableFonts = useMemo(
() => allFonts.filter(
(font) => !sceneFamilies.has(font.value) && (showDeprecatedFonts || !font.deprecated)
// skip deprecated fonts
),
[allFonts, sceneFamilies, showDeprecatedFonts]
);
const filteredFonts = useMemo(
() => arrayToList(
[...sceneFonts, ...availableFonts].filter(
(font) => font.text?.toLowerCase().includes(searchTerm)
)
),
[sceneFonts, availableFonts, searchTerm]
);
const hoveredFont = useMemo(() => {
let font;
if (hoveredFontFamily) {
font = filteredFonts.find((font2) => font2.value === hoveredFontFamily);
} else if (selectedFontFamily) {
font = filteredFonts.find((font2) => font2.value === selectedFontFamily);
}
if (!font && searchTerm) {
if (filteredFonts[0]?.value) {
onHover(filteredFonts[0].value);
} else {
onLeave();
}
}
return font;
}, [
hoveredFontFamily,
selectedFontFamily,
searchTerm,
filteredFonts,
onHover,
onLeave
]);
const onKeyDown = useCallback2(
(event) => {
const handled = fontPickerKeyHandler({
event,
inputRef,
hoveredFont,
filteredFonts,
onSelect,
onHover,
onClose
});
if (handled) {
event.preventDefault();
event.stopPropagation();
}
},
[hoveredFont, filteredFonts, onSelect, onHover, onClose]
);
useEffect13(() => {
onOpen();
return () => {
onClose();
};
}, []);
const sceneFilteredFonts = useMemo(
() => filteredFonts.filter((font) => sceneFamilies.has(font.value)),
[filteredFonts, sceneFamilies]
);
const availableFilteredFonts = useMemo(
() => filteredFonts.filter((font) => !sceneFamilies.has(font.value)),
[filteredFonts, sceneFamilies]
);
const renderFont = (font, index) => /* @__PURE__ */ jsxs17(
DropdownMenuItem_default,
{
icon: font.icon,
value: font.value,
order: index,
textStyle: {
fontFamily: getFontFamilyString({ fontFamily: font.value })
},
hovered: font.value === hoveredFont?.value,
selected: font.value === selectedFontFamily,
tabIndex: font.value === selectedFontFamily ? 0 : -1,
onClick: (e) => {
onSelect(Number(e.currentTarget.value));
},
onMouseMove: () => {
if (hoveredFont?.value !== font.value) {
onHover(font.value);
}
},
children: [
font.text,
font.badge && /* @__PURE__ */ jsx30(DropDownMenuItemBadge, { type: font.badge.type, children: font.badge.placeholder })
]
},
font.value
);
const groups = [];
if (sceneFilteredFonts.length) {
groups.push(
/* @__PURE__ */ jsx30(DropdownMenuGroup_default, { title: t("fontList.sceneFonts"), children: sceneFilteredFonts.map(renderFont) }, "group_1")
);
}
if (availableFilteredFonts.length) {
groups.push(
/* @__PURE__ */ jsx30(DropdownMenuGroup_default, { title: t("fontList.availableFonts"), children: availableFilteredFonts.map(
(font, index) => renderFont(font, index + sceneFilteredFonts.length)
) }, "group_2")
);
}
return /* @__PURE__ */ jsxs17(
PropertiesPopover,
{
className: "properties-content",
container,
style: { width: "15rem" },
onClose,
onPointerLeave: onLeave,
onKeyDown,
children: [
/* @__PURE__ */ jsx30(
QuickSearch,
{
ref: inputRef,
placeholder: t("quickSearch.placeholder"),
onChange: debounce(setSearchTerm, 20)
}
),
/* @__PURE__ */ jsx30(
ScrollableList,
{
className: "dropdown-menu fonts manual-hover",
placeholder: t("fontList.empty"),
children: groups.length ? groups : null
}
)
]
}
);
},
(prev, next) => prev.selectedFontFamily === next.selectedFontFamily && prev.hoveredFontFamily === next.hoveredFontFamily
);
// components/FontPicker/FontPickerTrigger.tsx
import * as Popover4 from "@radix-ui/react-popover";
import { useMemo as useMemo2 } from "react";
import { jsx as jsx31 } from "react/jsx-runtime";
var FontPickerTrigger = ({
selectedFontFamily
}) => {
const isTriggerActive = useMemo2(
() => Boolean(selectedFontFamily && !isDefaultFont(selectedFontFamily)),
[selectedFontFamily]
);
return /* @__PURE__ */ jsx31(Popover4.Trigger, { asChild: true, children: /* @__PURE__ */ jsx31("div", { children: /* @__PURE__ */ jsx31(
ButtonIcon,
{
standalone: true,
icon: TextIcon,
title: t("labels.showFonts"),
className: "properties-trigger",
testId: "font-family-show-fonts",
active: isTriggerActive,
onClick: () => {
}
}
) }) });
};
// components/FontPicker/FontPicker.tsx
import { jsx as jsx32, jsxs as jsxs18 } from "react/jsx-runtime";
var DEFAULT_FONTS = [
{
value: FONT_FAMILY.Excalifont,
icon: FreedrawIcon,
text: t("labels.handDrawn"),
testId: "font-family-hand-drawn"
},
{
value: FONT_FAMILY.Nunito,
icon: FontFamilyNormalIcon,
text: t("labels.normal"),
testId: "font-family-normal"
},
{
value: FONT_FAMILY["Comic Shanns"],
icon: FontFamilyCodeIcon,
text: t("labels.code"),
testId: "font-family-code"
}
];
var defaultFontFamilies = new Set(DEFAULT_FONTS.map((x) => x.value));
var isDefaultFont = (fontFamily) => {
if (!fontFamily) {
return false;
}
return defaultFontFamilies.has(fontFamily);
};
var FontPicker = React13.memo(
({
isOpened,
selectedFontFamily,
hoveredFontFamily,
onSelect,
onHover,
onLeave,
onPopupChange
}) => {
const defaultFonts = useMemo3(() => DEFAULT_FONTS, []);
const onSelectCallback = useCallback3(
(value) => {
if (value) {
onSelect(value);
}
},
[onSelect]
);
return /* @__PURE__ */ jsxs18("div", { role: "dialog", "aria-modal": "true", className: "FontPicker__container", children: [
/* @__PURE__ */ jsx32(
ButtonIconSelect,
{
type: "button",
options: defaultFonts,
value: selectedFontFamily,
onClick: onSelectCallback
}
),
/* @__PURE__ */ jsx32(ButtonSeparator, {}),
/* @__PURE__ */ jsxs18(Popover5.Root, { open: isOpened, onOpenChange: onPopupChange, children: [
/* @__PURE__ */ jsx32(FontPickerTrigger, { selectedFontFamily }),
isOpened && /* @__PURE__ */ jsx32(
FontPickerList,
{
selectedFontFamily,
hoveredFontFamily,
onSelect: onSelectCallback,
onHover,
onLeave,
onOpen: () => onPopupChange(true),
onClose: () => onPopupChange(false)
}
)
] })
] });
},
(prev, next) => prev.isOpened === next.isOpened && prev.selectedFontFamily === next.selectedFontFamily && prev.hoveredFontFamily === next.hoveredFontFamily
);
// components/Range.tsx
import React14, { useEffect as useEffect14 } from "react";
import { jsx as jsx33, jsxs as jsxs19 } from "react/jsx-runtime";
var Range = ({
updateData,
appState,
elements,
testId
}) => {
const rangeRef = React14.useRef(null);
const valueRef = React14.useRef(null);
const value = getFormValue(
elements,
appState,
(element) => element.opacity,
true,
appState.currentItemOpacity
);
useEffect14(() => {
if (rangeRef.current && valueRef.current) {
const rangeElement = rangeRef.current;
const valueElement = valueRef.current;
const inputWidth = rangeElement.offsetWidth;
const thumbWidth = 15;
const position = value / 100 * (inputWidth - thumbWidth) + thumbWidth / 2;
valueElement.style.left = `${position}px`;
rangeElement.style.background = `linear-gradient(to right, var(--color-slider-track) 0%, var(--color-slider-track) ${value}%, var(--button-bg) ${value}%, var(--button-bg) 100%)`;
}
}, [value]);
return /* @__PURE__ */ jsxs19("label", { className: "control-label", children: [
t("labels.opacity"),
/* @__PURE__ */ jsxs19("div", { className: "range-wrapper", children: [
/* @__PURE__ */ jsx33(
"input",
{
ref: rangeRef,
type: "range",
min: "0",
max: "100",
step: "10",
onChange: (event) => {
updateData(+event.target.value);
},
value,
className: "range-input",
"data-testid": testId
}
),
/* @__PURE__ */ jsx33("div", { className: "value-bubble", ref: valueRef, children: value !== 0 ? value : null }),
/* @__PURE__ */ jsx33("div", { className: "zero-label", children: "0" })
] })
] });
};
// actions/actionProperties.tsx
import { Fragment as Fragment4, jsx as jsx34, jsxs as jsxs20 } from "react/jsx-runtime";
var FONT_SIZE_RELATIVE_INCREASE_STEP = 0.1;
var changeProperty = (elements, appState, callback, includeBoundText = false) => {
const selectedElementIds = arrayToMap(
getSelectedElements(elements, appState, {
includeBoundTextElement: includeBoundText
})
);
return elements.map((element) => {
if (selectedElementIds.get(element.id) || element.id === appState.editingTextElement?.id) {
return callback(element);
}
return element;
});
};
var getFormValue = function(elements, appState, getAttribute, isRelevantElement, defaultValue) {
const editingTextElement = appState.editingTextElement;
const nonDeletedElements = getNonDeletedElements(elements);
let ret = null;
if (editingTextElement) {
ret = getAttribute(editingTextElement);
}
if (!ret) {
const hasSelection = isSomeElementSelected(nonDeletedElements, appState);
if (hasSelection) {
ret = getCommonAttributeOfSelectedElements(
isRelevantElement === true ? nonDeletedElements : nonDeletedElements.filter((el) => isRelevantElement(el)),
appState,
getAttribute
) ?? (typeof defaultValue === "function" ? defaultValue(true) : defaultValue);
} else {
ret = typeof defaultValue === "function" ? defaultValue(false) : defaultValue;
}
}
return ret;
};
var offsetElementAfterFontResize = (prevElement, nextElement) => {
if (isBoundToContainer(nextElement) || !nextElement.autoResize) {
return nextElement;
}
return mutateElement(
nextElement,
{
x: prevElement.textAlign === "left" ? prevElement.x : prevElement.x + (prevElement.width - nextElement.width) / (prevElement.textAlign === "center" ? 2 : 1),
// centering vertically is non-standard, but for Excalidraw I think
// it makes sense
y: prevElement.y + (prevElement.height - nextElement.height) / 2
},
false
);
};
var changeFontSize = (elements, appState, app, getNewFontSize, fallbackValue) => {
const newFontSizes = /* @__PURE__ */ new Set();
const updatedElements = changeProperty(
elements,
appState,
(oldElement) => {
if (isTextElement(oldElement)) {
const newFontSize = getNewFontSize(oldElement);
newFontSizes.add(newFontSize);
let newElement2 = newElementWith(oldElement, {
fontSize: newFontSize
});
redrawTextBoundingBox(
newElement2,
app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap()
);
newElement2 = offsetElementAfterFontResize(oldElement, newElement2);
return newElement2;
}
return oldElement;
},
true
);
const updatedElementsMap = arrayToMap(updatedElements);
getSelectedElements(elements, appState, {
includeBoundTextElement: true
}).forEach((element) => {
if (isTextElement(element)) {
updateBoundElements(
element,
updatedElementsMap
);
}
});
return {
elements: updatedElements,
appState: {
...appState,
// update state only if we've set all select text elements to
// the same font size
currentItemFontSize: newFontSizes.size === 1 ? [...newFontSizes][0] : fallbackValue ?? appState.currentItemFontSize
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
};
var actionChangeStrokeColor = register({
name: "changeStrokeColor",
label: "labels.stroke",
trackEvent: false,
perform: (elements, appState, value) => {
return {
...value.currentItemStrokeColor && {
elements: changeProperty(
elements,
appState,
(el) => {
return hasStrokeColor(el.type) ? newElementWith(el, {
strokeColor: value.currentItemStrokeColor
}) : el;
},
true
)
},
appState: {
...appState,
...value
},
captureUpdate: !!value.currentItemStrokeColor ? CaptureUpdateAction.IMMEDIATELY : CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ elements, appState, updateData, appProps }) => /* @__PURE__ */ jsxs20(Fragment4, { children: [
/* @__PURE__ */ jsx34("h3", { "aria-hidden": "true", children: t("labels.stroke") }),
/* @__PURE__ */ jsx34(
ColorPicker,
{
topPicks: DEFAULT_ELEMENT_STROKE_PICKS,
palette: DEFAULT_ELEMENT_STROKE_COLOR_PALETTE,
type: "elementStroke",
label: t("labels.stroke"),
color: getFormValue(
elements,
appState,
(element) => element.strokeColor,
true,
appState.currentItemStrokeColor
),
onChange: (color) => updateData({ currentItemStrokeColor: color }),
elements,
appState,
updateData
}
)
] })
});
var actionChangeBackgroundColor = register({
name: "changeBackgroundColor",
label: "labels.changeBackground",
trackEvent: false,
perform: (elements, appState, value) => {
return {
...value.currentItemBackgroundColor && {
elements: changeProperty(
elements,
appState,
(el) => newElementWith(el, {
backgroundColor: value.currentItemBackgroundColor
})
)
},
appState: {
...appState,
...value
},
captureUpdate: !!value.currentItemBackgroundColor ? CaptureUpdateAction.IMMEDIATELY : CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ elements, appState, updateData, appProps }) => /* @__PURE__ */ jsxs20(Fragment4, { children: [
/* @__PURE__ */ jsx34("h3", { "aria-hidden": "true", children: t("labels.background") }),
/* @__PURE__ */ jsx34(
ColorPicker,
{
topPicks: DEFAULT_ELEMENT_BACKGROUND_PICKS,
palette: DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
type: "elementBackground",
label: t("labels.background"),
color: getFormValue(
elements,
appState,
(element) => element.backgroundColor,
true,
appState.currentItemBackgroundColor
),
onChange: (color) => updateData({ currentItemBackgroundColor: color }),
elements,
appState,
updateData
}
)
] })
});
var actionChangeFillStyle = register({
name: "changeFillStyle",
label: "labels.fill",
trackEvent: false,
perform: (elements, appState, value, app) => {
trackEvent(
"element",
"changeFillStyle",
`${value} (${app.device.editor.isMobile ? "mobile" : "desktop"})`
);
return {
elements: changeProperty(
elements,
appState,
(el) => newElementWith(el, {
fillStyle: value
})
),
appState: { ...appState, currentItemFillStyle: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData }) => {
const selectedElements = getSelectedElements(elements, appState);
const allElementsZigZag = selectedElements.length > 0 && selectedElements.every((el) => el.fillStyle === "zigzag");
return /* @__PURE__ */ jsxs20("fieldset", { children: [
/* @__PURE__ */ jsx34("legend", { children: t("labels.fill") }),
/* @__PURE__ */ jsx34(
ButtonIconSelect,
{
type: "button",
options: [
{
value: "hachure",
text: `${allElementsZigZag ? t("labels.zigzag") : t("labels.hachure")} (${getShortcutKey("Alt-Click")})`,
icon: allElementsZigZag ? FillZigZagIcon : FillHachureIcon,
active: allElementsZigZag ? true : void 0,
testId: `fill-hachure`
},
{
value: "cross-hatch",
text: t("labels.crossHatch"),
icon: FillCrossHatchIcon,
testId: `fill-cross-hatch`
},
{
value: "solid",
text: t("labels.solid"),
icon: FillSolidIcon,
testId: `fill-solid`
}
],
value: getFormValue(
elements,
appState,
(element) => element.fillStyle,
(element) => element.hasOwnProperty("fillStyle"),
(hasSelection) => hasSelection ? null : appState.currentItemFillStyle
),
onClick: (value, event) => {
const nextValue = event.altKey && value === "hachure" && selectedElements.every((el) => el.fillStyle === "hachure") ? "zigzag" : value;
updateData(nextValue);
}
}
)
] });
}
});
var actionChangeStrokeWidth = register({
name: "changeStrokeWidth",
label: "labels.strokeWidth",
trackEvent: false,
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
appState,
(el) => newElementWith(el, {
strokeWidth: value
})
),
appState: { ...appState, currentItemStrokeWidth: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData }) => /* @__PURE__ */ jsxs20("fieldset", { children: [
/* @__PURE__ */ jsx34("legend", { children: t("labels.strokeWidth") }),
/* @__PURE__ */ jsx34(
ButtonIconSelect,
{
group: "stroke-width",
options: [
{
value: STROKE_WIDTH.thin,
text: t("labels.thin"),
icon: StrokeWidthBaseIcon,
testId: "strokeWidth-thin"
},
{
value: STROKE_WIDTH.bold,
text: t("labels.bold"),
icon: StrokeWidthBoldIcon,
testId: "strokeWidth-bold"
},
{
value: STROKE_WIDTH.extraBold,
text: t("labels.extraBold"),
icon: StrokeWidthExtraBoldIcon,
testId: "strokeWidth-extraBold"
}
],
value: getFormValue(
elements,
appState,
(element) => element.strokeWidth,
(element) => element.hasOwnProperty("strokeWidth"),
(hasSelection) => hasSelection ? null : appState.currentItemStrokeWidth
),
onChange: (value) => updateData(value)
}
)
] })
});
var actionChangeSloppiness = register({
name: "changeSloppiness",
label: "labels.sloppiness",
trackEvent: false,
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
appState,
(el) => newElementWith(el, {
seed: randomInteger(),
roughness: value
})
),
appState: { ...appState, currentItemRoughness: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData }) => /* @__PURE__ */ jsxs20("fieldset", { children: [
/* @__PURE__ */ jsx34("legend", { children: t("labels.sloppiness") }),
/* @__PURE__ */ jsx34(
ButtonIconSelect,
{
group: "sloppiness",
options: [
{
value: 0,
text: t("labels.architect"),
icon: SloppinessArchitectIcon
},
{
value: 1,
text: t("labels.artist"),
icon: SloppinessArtistIcon
},
{
value: 2,
text: t("labels.cartoonist"),
icon: SloppinessCartoonistIcon
}
],
value: getFormValue(
elements,
appState,
(element) => element.roughness,
(element) => element.hasOwnProperty("roughness"),
(hasSelection) => hasSelection ? null : appState.currentItemRoughness
),
onChange: (value) => updateData(value)
}
)
] })
});
var actionChangeStrokeStyle = register({
name: "changeStrokeStyle",
label: "labels.strokeStyle",
trackEvent: false,
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
appState,
(el) => newElementWith(el, {
strokeStyle: value
})
),
appState: { ...appState, currentItemStrokeStyle: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData }) => /* @__PURE__ */ jsxs20("fieldset", { children: [
/* @__PURE__ */ jsx34("legend", { children: t("labels.strokeStyle") }),
/* @__PURE__ */ jsx34(
ButtonIconSelect,
{
group: "strokeStyle",
options: [
{
value: "solid",
text: t("labels.strokeStyle_solid"),
icon: StrokeWidthBaseIcon
},
{
value: "dashed",
text: t("labels.strokeStyle_dashed"),
icon: StrokeStyleDashedIcon
},
{
value: "dotted",
text: t("labels.strokeStyle_dotted"),
icon: StrokeStyleDottedIcon
}
],
value: getFormValue(
elements,
appState,
(element) => element.strokeStyle,
(element) => element.hasOwnProperty("strokeStyle"),
(hasSelection) => hasSelection ? null : appState.currentItemStrokeStyle
),
onChange: (value) => updateData(value)
}
)
] })
});
var actionChangeOpacity = register({
name: "changeOpacity",
label: "labels.opacity",
trackEvent: false,
perform: (elements, appState, value) => {
return {
elements: changeProperty(
elements,
appState,
(el) => newElementWith(el, {
opacity: value
}),
true
),
appState: { ...appState, currentItemOpacity: value },
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData }) => /* @__PURE__ */ jsx34(
Range,
{
updateData,
elements,
appState,
testId: "opacity"
}
)
});
var actionChangeFontSize = register({
name: "changeFontSize",
label: "labels.fontSize",
trackEvent: false,
perform: (elements, appState, value, app) => {
return changeFontSize(elements, appState, app, () => value, value);
},
PanelComponent: ({ elements, appState, updateData, app }) => /* @__PURE__ */ jsxs20("fieldset", { children: [
/* @__PURE__ */ jsx34("legend", { children: t("labels.fontSize") }),
/* @__PURE__ */ jsx34(
ButtonIconSelect,
{
group: "font-size",
options: [
{
value: 16,
text: t("labels.small"),
icon: FontSizeSmallIcon,
testId: "fontSize-small"
},
{
value: 20,
text: t("labels.medium"),
icon: FontSizeMediumIcon,
testId: "fontSize-medium"
},
{
value: 28,
text: t("labels.large"),
icon: FontSizeLargeIcon,
testId: "fontSize-large"
},
{
value: 36,
text: t("labels.veryLarge"),
icon: FontSizeExtraLargeIcon,
testId: "fontSize-veryLarge"
}
],
value: getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element)) {
return element.fontSize;
}
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap()
);
if (boundTextElement) {
return boundTextElement.fontSize;
}
return null;
},
(element) => isTextElement(element) || getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap()
) !== null,
(hasSelection) => hasSelection ? null : appState.currentItemFontSize || DEFAULT_FONT_SIZE
),
onChange: (value) => updateData(value)
}
)
] })
});
var actionDecreaseFontSize = register({
name: "decreaseFontSize",
label: "labels.decreaseFontSize",
icon: fontSizeIcon,
trackEvent: false,
perform: (elements, appState, value, app) => {
return changeFontSize(
elements,
appState,
app,
(element) => Math.round(
// get previous value before relative increase (doesn't work fully
// due to rounding and float precision issues)
1 / (1 + FONT_SIZE_RELATIVE_INCREASE_STEP) * element.fontSize
)
);
},
keyTest: (event) => {
return event[KEYS.CTRL_OR_CMD] && event.shiftKey && // KEYS.COMMA needed for MacOS
(event.key === KEYS.CHEVRON_LEFT || event.key === KEYS.COMMA);
}
});
var actionIncreaseFontSize = register({
name: "increaseFontSize",
label: "labels.increaseFontSize",
icon: fontSizeIcon,
trackEvent: false,
perform: (elements, appState, value, app) => {
return changeFontSize(
elements,
appState,
app,
(element) => Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP))
);
},
keyTest: (event) => {
return event[KEYS.CTRL_OR_CMD] && event.shiftKey && // KEYS.PERIOD needed for MacOS
(event.key === KEYS.CHEVRON_RIGHT || event.key === KEYS.PERIOD);
}
});
var actionChangeFontFamily = register({
name: "changeFontFamily",
label: "labels.fontFamily",
trackEvent: false,
perform: (elements, appState, value, app) => {
const { cachedElements, resetAll, resetContainers, ...nextAppState } = value;
if (resetAll) {
const nextElements = changeProperty(
elements,
appState,
(element) => {
const cachedElement = cachedElements?.get(element.id);
if (cachedElement) {
const newElement2 = newElementWith(element, {
...cachedElement
});
return newElement2;
}
return element;
},
true
);
return {
elements: nextElements,
appState: {
...appState,
...nextAppState
},
captureUpdate: CaptureUpdateAction.NEVER
};
}
const { currentItemFontFamily, currentHoveredFontFamily } = value;
let nextCaptureUpdateAction = CaptureUpdateAction.EVENTUALLY;
let nextFontFamily;
let skipOnHoverRender = false;
if (currentItemFontFamily) {
nextFontFamily = currentItemFontFamily;
nextCaptureUpdateAction = CaptureUpdateAction.IMMEDIATELY;
} else if (currentHoveredFontFamily) {
nextFontFamily = currentHoveredFontFamily;
nextCaptureUpdateAction = CaptureUpdateAction.EVENTUALLY;
const selectedTextElements = getSelectedElements(elements, appState, {
includeBoundTextElement: true
}).filter((element) => isTextElement(element));
if (selectedTextElements.length > 200) {
skipOnHoverRender = true;
} else {
let i = 0;
let textLengthAccumulator = 0;
while (i < selectedTextElements.length && textLengthAccumulator < 5e3) {
const textElement = selectedTextElements[i];
textLengthAccumulator += textElement?.originalText.length || 0;
i++;
}
if (textLengthAccumulator > 5e3) {
skipOnHoverRender = true;
}
}
}
const result = {
appState: {
...appState,
...nextAppState
},
captureUpdate: nextCaptureUpdateAction
};
if (nextFontFamily && !skipOnHoverRender) {
const elementContainerMapping = /* @__PURE__ */ new Map();
let uniqueChars = /* @__PURE__ */ new Set();
let skipFontFaceCheck = false;
const fontsCache = Array.from(Fonts.loadedFontsCache.values());
const fontFamily = Object.entries(FONT_FAMILY).find(
([_, value2]) => value2 === nextFontFamily
)?.[0];
if (currentHoveredFontFamily && fontFamily && fontsCache.some((sig) => sig.startsWith(fontFamily))) {
skipFontFaceCheck = true;
}
Object.assign(result, {
elements: changeProperty(
elements,
appState,
(oldElement) => {
if (isTextElement(oldElement) && (oldElement.fontFamily !== nextFontFamily || currentItemFontFamily)) {
const newElement2 = newElementWith(
oldElement,
{
fontFamily: nextFontFamily,
lineHeight: getLineHeight(nextFontFamily)
}
);
const cachedContainer = cachedElements?.get(oldElement.containerId || "") || {};
const container = app.scene.getContainerElement(oldElement);
if (resetContainers && container && cachedContainer) {
mutateElement(container, { ...cachedContainer }, false);
}
if (!skipFontFaceCheck) {
uniqueChars = /* @__PURE__ */ new Set([
...uniqueChars,
...Array.from(newElement2.originalText)
]);
}
elementContainerMapping.set(newElement2, container);
return newElement2;
}
return oldElement;
},
true
)
});
const fontString = `10px ${getFontFamilyString({
fontFamily: nextFontFamily
})}`;
const chars = Array.from(uniqueChars.values()).join();
if (skipFontFaceCheck || window.document.fonts.check(fontString, chars)) {
for (const [element, container] of elementContainerMapping) {
redrawTextBoundingBox(
element,
container,
app.scene.getNonDeletedElementsMap(),
false
);
}
} else {
window.document.fonts.load(fontString, chars).then((fontFaces) => {
for (const [element, container] of elementContainerMapping) {
const latestElement = app.scene.getElement(element.id);
const latestContainer = container ? app.scene.getElement(container.id) : null;
if (latestElement) {
redrawTextBoundingBox(
latestElement,
latestContainer,
app.scene.getNonDeletedElementsMap(),
false
);
}
}
app.fonts.onLoaded(fontFaces);
});
}
}
return result;
},
PanelComponent: ({ elements, appState, app, updateData }) => {
const cachedElementsRef = useRef11(/* @__PURE__ */ new Map());
const prevSelectedFontFamilyRef = useRef11(null);
const [batchedData, setBatchedData] = useState8({});
const isUnmounted = useRef11(true);
const selectedFontFamily = useMemo4(() => {
const getFontFamily = (elementsArray, elementsMap) => getFormValue(
elementsArray,
appState,
(element) => {
if (isTextElement(element)) {
return element.fontFamily;
}
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
return boundTextElement.fontFamily;
}
return null;
},
(element) => isTextElement(element) || getBoundTextElement(element, elementsMap) !== null,
(hasSelection) => hasSelection ? null : appState.currentItemFontFamily || DEFAULT_FONT_FAMILY
);
if (batchedData.openPopup === "fontFamily" && appState.openPopup === "fontFamily") {
return getFontFamily(
Array.from(cachedElementsRef.current?.values() ?? []),
cachedElementsRef.current
);
}
if (!batchedData.openPopup && appState.openPopup !== "fontFamily") {
return getFontFamily(elements, app.scene.getNonDeletedElementsMap());
}
return prevSelectedFontFamilyRef.current;
}, [batchedData.openPopup, appState, elements, app.scene]);
useEffect15(() => {
prevSelectedFontFamilyRef.current = selectedFontFamily;
}, [selectedFontFamily]);
useEffect15(() => {
if (Object.keys(batchedData).length) {
updateData(batchedData);
setBatchedData({});
}
}, [batchedData]);
useEffect15(() => {
isUnmounted.current = false;
return () => {
isUnmounted.current = true;
};
}, []);
return /* @__PURE__ */ jsxs20("fieldset", { children: [
/* @__PURE__ */ jsx34("legend", { children: t("labels.fontFamily") }),
/* @__PURE__ */ jsx34(
FontPicker,
{
isOpened: appState.openPopup === "fontFamily",
selectedFontFamily,
hoveredFontFamily: appState.currentHoveredFontFamily,
onSelect: (fontFamily) => {
setBatchedData({
openPopup: null,
currentHoveredFontFamily: null,
currentItemFontFamily: fontFamily
});
cachedElementsRef.current.clear();
},
onHover: (fontFamily) => {
setBatchedData({
currentHoveredFontFamily: fontFamily,
cachedElements: new Map(cachedElementsRef.current),
resetContainers: true
});
},
onLeave: () => {
setBatchedData({
currentHoveredFontFamily: null,
cachedElements: new Map(cachedElementsRef.current),
resetAll: true
});
},
onPopupChange: (open) => {
if (open) {
cachedElementsRef.current.clear();
const { editingTextElement } = appState;
if (editingTextElement?.type === "text") {
const latesteditingTextElement = app.scene.getElement(
editingTextElement.id
);
cachedElementsRef.current.set(
editingTextElement.id,
newElementWith(
latesteditingTextElement || editingTextElement,
{},
true
)
);
} else {
const selectedElements = getSelectedElements(
elements,
appState,
{
includeBoundTextElement: true
}
);
for (const element of selectedElements) {
cachedElementsRef.current.set(
element.id,
newElementWith(element, {}, true)
);
}
}
setBatchedData({
openPopup: "fontFamily"
});
} else {
const data = {
openPopup: null,
currentHoveredFontFamily: null,
cachedElements: new Map(cachedElementsRef.current),
resetAll: true
};
if (isUnmounted.current) {
updateData({ ...batchedData, ...data });
} else {
setBatchedData(data);
}
cachedElementsRef.current.clear();
}
}
}
)
] });
}
});
var actionChangeTextAlign = register({
name: "changeTextAlign",
label: "Change text alignment",
trackEvent: false,
perform: (elements, appState, value, app) => {
return {
elements: changeProperty(
elements,
appState,
(oldElement) => {
if (isTextElement(oldElement)) {
const newElement2 = newElementWith(
oldElement,
{ textAlign: value }
);
redrawTextBoundingBox(
newElement2,
app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap()
);
return newElement2;
}
return oldElement;
},
true
),
appState: {
...appState,
currentItemTextAlign: value
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData, app }) => {
const elementsMap = app.scene.getNonDeletedElementsMap();
return /* @__PURE__ */ jsxs20("fieldset", { children: [
/* @__PURE__ */ jsx34("legend", { children: t("labels.textAlign") }),
/* @__PURE__ */ jsx34(
ButtonIconSelect,
{
group: "text-align",
options: [
{
value: "left",
text: t("labels.left"),
icon: TextAlignLeftIcon,
testId: "align-left"
},
{
value: "center",
text: t("labels.center"),
icon: TextAlignCenterIcon,
testId: "align-horizontal-center"
},
{
value: "right",
text: t("labels.right"),
icon: TextAlignRightIcon,
testId: "align-right"
}
],
value: getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element)) {
return element.textAlign;
}
const boundTextElement = getBoundTextElement(
element,
elementsMap
);
if (boundTextElement) {
return boundTextElement.textAlign;
}
return null;
},
(element) => isTextElement(element) || getBoundTextElement(element, elementsMap) !== null,
(hasSelection) => hasSelection ? null : appState.currentItemTextAlign
),
onChange: (value) => updateData(value)
}
)
] });
}
});
var actionChangeVerticalAlign = register({
name: "changeVerticalAlign",
label: "Change vertical alignment",
trackEvent: { category: "element" },
perform: (elements, appState, value, app) => {
return {
elements: changeProperty(
elements,
appState,
(oldElement) => {
if (isTextElement(oldElement)) {
const newElement2 = newElementWith(
oldElement,
{ verticalAlign: value }
);
redrawTextBoundingBox(
newElement2,
app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap()
);
return newElement2;
}
return oldElement;
},
true
),
appState: {
...appState
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData, app }) => {
return /* @__PURE__ */ jsx34("fieldset", { children: /* @__PURE__ */ jsx34(
ButtonIconSelect,
{
group: "text-align",
options: [
{
value: VERTICAL_ALIGN.TOP,
text: t("labels.alignTop"),
icon: /* @__PURE__ */ jsx34(TextAlignTopIcon, { theme: appState.theme }),
testId: "align-top"
},
{
value: VERTICAL_ALIGN.MIDDLE,
text: t("labels.centerVertically"),
icon: /* @__PURE__ */ jsx34(TextAlignMiddleIcon, { theme: appState.theme }),
testId: "align-middle"
},
{
value: VERTICAL_ALIGN.BOTTOM,
text: t("labels.alignBottom"),
icon: /* @__PURE__ */ jsx34(TextAlignBottomIcon, { theme: appState.theme }),
testId: "align-bottom"
}
],
value: getFormValue(
elements,
appState,
(element) => {
if (isTextElement(element) && element.containerId) {
return element.verticalAlign;
}
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap()
);
if (boundTextElement) {
return boundTextElement.verticalAlign;
}
return null;
},
(element) => isTextElement(element) || getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap()
) !== null,
(hasSelection) => hasSelection ? null : VERTICAL_ALIGN.MIDDLE
),
onChange: (value) => updateData(value)
}
) });
}
});
var actionChangeRoundness = register({
name: "changeRoundness",
label: "Change edge roundness",
trackEvent: false,
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, appState, (el) => {
if (isElbowArrow(el)) {
return el;
}
return newElementWith(el, {
roundness: value === "round" ? {
type: isUsingAdaptiveRadius(el.type) ? ROUNDNESS.ADAPTIVE_RADIUS : ROUNDNESS.PROPORTIONAL_RADIUS
} : null
});
}),
appState: {
...appState,
currentItemRoundness: value
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData }) => {
const targetElements = getTargetElements(
getNonDeletedElements(elements),
appState
);
const hasLegacyRoundness = targetElements.some(
(el) => el.roundness?.type === ROUNDNESS.LEGACY
);
return /* @__PURE__ */ jsxs20("fieldset", { children: [
/* @__PURE__ */ jsx34("legend", { children: t("labels.edges") }),
/* @__PURE__ */ jsx34(
ButtonIconSelect,
{
group: "edges",
options: [
{
value: "sharp",
text: t("labels.sharp"),
icon: EdgeSharpIcon
},
{
value: "round",
text: t("labels.round"),
icon: EdgeRoundIcon
}
],
value: getFormValue(
elements,
appState,
(element) => hasLegacyRoundness ? null : element.roundness ? "round" : "sharp",
(element) => !isArrowElement(element) && element.hasOwnProperty("roundness"),
(hasSelection) => hasSelection ? null : appState.currentItemRoundness
),
onChange: (value) => updateData(value)
}
)
] });
}
});
var getArrowheadOptions = (flip) => {
return [
{
value: null,
text: t("labels.arrowhead_none"),
keyBinding: "q",
icon: ArrowheadNoneIcon
},
{
value: "arrow",
text: t("labels.arrowhead_arrow"),
keyBinding: "w",
icon: /* @__PURE__ */ jsx34(ArrowheadArrowIcon, { flip })
},
{
value: "triangle",
text: t("labels.arrowhead_triangle"),
icon: /* @__PURE__ */ jsx34(ArrowheadTriangleIcon, { flip }),
keyBinding: "e"
},
{
value: "triangle_outline",
text: t("labels.arrowhead_triangle_outline"),
icon: /* @__PURE__ */ jsx34(ArrowheadTriangleOutlineIcon, { flip }),
keyBinding: "r"
},
{
value: "circle",
text: t("labels.arrowhead_circle"),
keyBinding: "a",
icon: /* @__PURE__ */ jsx34(ArrowheadCircleIcon, { flip })
},
{
value: "circle_outline",
text: t("labels.arrowhead_circle_outline"),
keyBinding: "s",
icon: /* @__PURE__ */ jsx34(ArrowheadCircleOutlineIcon, { flip })
},
{
value: "diamond",
text: t("labels.arrowhead_diamond"),
icon: /* @__PURE__ */ jsx34(ArrowheadDiamondIcon, { flip }),
keyBinding: "d"
},
{
value: "diamond_outline",
text: t("labels.arrowhead_diamond_outline"),
icon: /* @__PURE__ */ jsx34(ArrowheadDiamondOutlineIcon, { flip }),
keyBinding: "f"
},
{
value: "bar",
text: t("labels.arrowhead_bar"),
keyBinding: "z",
icon: /* @__PURE__ */ jsx34(ArrowheadBarIcon, { flip })
},
{
value: "crowfoot_one",
text: t("labels.arrowhead_crowfoot_one"),
icon: /* @__PURE__ */ jsx34(ArrowheadCrowfootOneIcon, { flip }),
keyBinding: "c"
},
{
value: "crowfoot_many",
text: t("labels.arrowhead_crowfoot_many"),
icon: /* @__PURE__ */ jsx34(ArrowheadCrowfootIcon, { flip }),
keyBinding: "x"
},
{
value: "crowfoot_one_or_many",
text: t("labels.arrowhead_crowfoot_one_or_many"),
icon: /* @__PURE__ */ jsx34(ArrowheadCrowfootOneOrManyIcon, { flip }),
keyBinding: "v"
}
];
};
var actionChangeArrowhead = register({
name: "changeArrowhead",
label: "Change arrowheads",
trackEvent: false,
perform: (elements, appState, value) => {
return {
elements: changeProperty(elements, appState, (el) => {
if (isLinearElement(el)) {
const { position, type } = value;
if (position === "start") {
const element = newElementWith(el, {
startArrowhead: type
});
return element;
} else if (position === "end") {
const element = newElementWith(el, {
endArrowhead: type
});
return element;
}
}
return el;
}),
appState: {
...appState,
[value.position === "start" ? "currentItemStartArrowhead" : "currentItemEndArrowhead"]: value.type
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData }) => {
const isRTL = getLanguage().rtl;
return /* @__PURE__ */ jsxs20("fieldset", { children: [
/* @__PURE__ */ jsx34("legend", { children: t("labels.arrowheads") }),
/* @__PURE__ */ jsxs20("div", { className: "iconSelectList buttonList", children: [
/* @__PURE__ */ jsx34(
IconPicker,
{
label: "arrowhead_start",
options: getArrowheadOptions(!isRTL),
value: getFormValue(
elements,
appState,
(element) => isLinearElement(element) && canHaveArrowheads(element.type) ? element.startArrowhead : appState.currentItemStartArrowhead,
true,
appState.currentItemStartArrowhead
),
onChange: (value) => updateData({ position: "start", type: value }),
numberOfOptionsToAlwaysShow: 4
}
),
/* @__PURE__ */ jsx34(
IconPicker,
{
label: "arrowhead_end",
group: "arrowheads",
options: getArrowheadOptions(!!isRTL),
value: getFormValue(
elements,
appState,
(element) => isLinearElement(element) && canHaveArrowheads(element.type) ? element.endArrowhead : appState.currentItemEndArrowhead,
true,
appState.currentItemEndArrowhead
),
onChange: (value) => updateData({ position: "end", type: value }),
numberOfOptionsToAlwaysShow: 4
}
)
] })
] });
}
});
var actionChangeArrowType = register({
name: "changeArrowType",
label: "Change arrow types",
trackEvent: false,
perform: (elements, appState, value, app) => {
const newElements = changeProperty(elements, appState, (el) => {
if (!isArrowElement(el)) {
return el;
}
const newElement2 = newElementWith(el, {
roundness: value === ARROW_TYPE.round ? {
type: ROUNDNESS.PROPORTIONAL_RADIUS
} : null,
elbowed: value === ARROW_TYPE.elbow,
points: value === ARROW_TYPE.elbow || el.elbowed ? [el.points[0], el.points[el.points.length - 1]] : el.points
});
if (isElbowArrow(newElement2)) {
const elementsMap = app.scene.getNonDeletedElementsMap();
app.dismissLinearEditor();
const startGlobalPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
newElement2,
0,
elementsMap
);
const endGlobalPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
newElement2,
-1,
elementsMap
);
const startHoveredElement = !newElement2.startBinding && getHoveredElementForBinding(
tupleToCoors(startGlobalPoint),
elements,
elementsMap,
appState.zoom,
false,
true
);
const endHoveredElement = !newElement2.endBinding && getHoveredElementForBinding(
tupleToCoors(endGlobalPoint),
elements,
elementsMap,
appState.zoom,
false,
true
);
const startElement = startHoveredElement ? startHoveredElement : newElement2.startBinding && elementsMap.get(
newElement2.startBinding.elementId
);
const endElement = endHoveredElement ? endHoveredElement : newElement2.endBinding && elementsMap.get(
newElement2.endBinding.elementId
);
const finalStartPoint = startHoveredElement ? bindPointToSnapToElementOutline(
newElement2,
startHoveredElement,
"start"
) : startGlobalPoint;
const finalEndPoint = endHoveredElement ? bindPointToSnapToElementOutline(
newElement2,
endHoveredElement,
"end"
) : endGlobalPoint;
startHoveredElement && bindLinearElement(
newElement2,
startHoveredElement,
"start",
elementsMap
);
endHoveredElement && bindLinearElement(newElement2, endHoveredElement, "end", elementsMap);
mutateElement(newElement2, {
points: [finalStartPoint, finalEndPoint].map(
(p) => pointFrom(p[0] - newElement2.x, p[1] - newElement2.y)
),
...startElement && newElement2.startBinding ? {
startBinding: {
// @ts-ignore TS cannot discern check above
...newElement2.startBinding,
...calculateFixedPointForElbowArrowBinding(
newElement2,
startElement,
"start",
elementsMap
)
}
} : {},
...endElement && newElement2.endBinding ? {
endBinding: {
// @ts-ignore TS cannot discern check above
...newElement2.endBinding,
...calculateFixedPointForElbowArrowBinding(
newElement2,
endElement,
"end",
elementsMap
)
}
} : {}
});
LinearElementEditor.updateEditorMidPointsCache(
newElement2,
elementsMap,
app.state
);
}
return newElement2;
});
const newState = {
...appState,
currentItemArrowType: value
};
const selectedId = appState.selectedLinearElement?.elementId;
if (selectedId) {
const selected = newElements.find((el) => el.id === selectedId);
if (selected) {
newState.selectedLinearElement = new LinearElementEditor(
selected
);
}
}
return {
elements: newElements,
appState: newState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData }) => {
return /* @__PURE__ */ jsxs20("fieldset", { children: [
/* @__PURE__ */ jsx34("legend", { children: t("labels.arrowtypes") }),
/* @__PURE__ */ jsx34(
ButtonIconSelect,
{
group: "arrowtypes",
options: [
{
value: ARROW_TYPE.sharp,
text: t("labels.arrowtype_sharp"),
icon: sharpArrowIcon,
testId: "sharp-arrow"
},
{
value: ARROW_TYPE.round,
text: t("labels.arrowtype_round"),
icon: roundArrowIcon,
testId: "round-arrow"
},
{
value: ARROW_TYPE.elbow,
text: t("labels.arrowtype_elbowed"),
icon: elbowArrowIcon,
testId: "elbow-arrow"
}
],
value: getFormValue(
elements,
appState,
(element) => {
if (isArrowElement(element)) {
return element.elbowed ? ARROW_TYPE.elbow : element.roundness ? ARROW_TYPE.round : ARROW_TYPE.sharp;
}
return null;
},
(element) => isArrowElement(element),
(hasSelection) => hasSelection ? null : appState.currentItemArrowType
),
onChange: (value) => updateData(value)
}
)
] });
}
});
// scene/zoom.ts
var getStateForZoom = ({
viewportX,
viewportY,
nextZoom
}, appState) => {
const appLayerX = viewportX - appState.offsetLeft;
const appLayerY = viewportY - appState.offsetTop;
const currentZoom = appState.zoom.value;
const baseScrollX = appState.scrollX + (appLayerX - appLayerX / currentZoom);
const baseScrollY = appState.scrollY + (appLayerY - appLayerY / currentZoom);
const zoomOffsetScrollX = -(appLayerX - appLayerX / nextZoom);
const zoomOffsetScrollY = -(appLayerY - appLayerY / nextZoom);
return {
scrollX: baseScrollX + zoomOffsetScrollX,
scrollY: baseScrollY + zoomOffsetScrollY,
zoom: {
value: nextZoom
}
};
};
// components/Tooltip.tsx
import { useEffect as useEffect16 } from "react";
import { jsx as jsx35 } from "react/jsx-runtime";
var getTooltipDiv = () => {
const existingDiv = document.querySelector(
".excalidraw-tooltip"
);
if (existingDiv) {
return existingDiv;
}
const div = document.createElement("div");
document.body.appendChild(div);
div.classList.add("excalidraw-tooltip");
return div;
};
var updateTooltipPosition = (tooltip, item, position = "bottom") => {
const tooltipRect = tooltip.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const margin = 5;
let left = item.left + item.width / 2 - tooltipRect.width / 2;
if (left < 0) {
left = margin;
} else if (left + tooltipRect.width >= viewportWidth) {
left = viewportWidth - tooltipRect.width - margin;
}
let top;
if (position === "bottom") {
top = item.top + item.height + margin;
if (top + tooltipRect.height >= viewportHeight) {
top = item.top - tooltipRect.height - margin;
}
} else {
top = item.top - tooltipRect.height - margin;
if (top < 0) {
top = item.top + item.height + margin;
}
}
Object.assign(tooltip.style, {
top: `${top}px`,
left: `${left}px`
});
};
var updateTooltip = (item, tooltip, label, long) => {
tooltip.classList.add("excalidraw-tooltip--visible");
tooltip.style.minWidth = long ? "50ch" : "10ch";
tooltip.style.maxWidth = long ? "50ch" : "15ch";
tooltip.textContent = label;
const itemRect = item.getBoundingClientRect();
updateTooltipPosition(tooltip, itemRect);
};
var Tooltip = ({
children,
label,
long = false,
style,
disabled
}) => {
useEffect16(() => {
return () => getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
}, []);
if (disabled) {
return null;
}
return /* @__PURE__ */ jsx35(
"div",
{
className: "excalidraw-tooltip-wrapper",
onPointerEnter: (event) => updateTooltip(
event.currentTarget,
getTooltipDiv(),
label,
long
),
onPointerLeave: () => getTooltipDiv().classList.remove("excalidraw-tooltip--visible"),
style,
children
}
);
};
// actions/actionCanvas.tsx
import { jsx as jsx36, jsxs as jsxs21 } from "react/jsx-runtime";
var actionChangeViewBackgroundColor = register({
name: "changeViewBackgroundColor",
label: "labels.canvasBackground",
paletteName: "Change canvas background color",
trackEvent: false,
predicate: (elements, appState, props, app) => {
return !!app.props.UIOptions.canvasActions.changeViewBackgroundColor && !appState.viewModeEnabled;
},
perform: (_, appState, value) => {
return {
appState: { ...appState, ...value },
captureUpdate: !!value.viewBackgroundColor ? CaptureUpdateAction.IMMEDIATELY : CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ elements, appState, updateData, appProps }) => {
return /* @__PURE__ */ jsx36(
ColorPicker,
{
palette: null,
topPicks: DEFAULT_CANVAS_BACKGROUND_PICKS,
label: t("labels.canvasBackground"),
type: "canvasBackground",
color: appState.viewBackgroundColor,
onChange: (color) => updateData({ viewBackgroundColor: color }),
"data-testid": "canvas-background-picker",
elements,
appState,
updateData
}
);
}
});
var actionClearCanvas = register({
name: "clearCanvas",
label: "labels.clearCanvas",
paletteName: "Clear canvas",
icon: TrashIcon,
trackEvent: { category: "canvas" },
predicate: (elements, appState, props, app) => {
return !!app.props.UIOptions.canvasActions.clearCanvas && !appState.viewModeEnabled && appState.openDialog?.name !== "elementLinkSelector";
},
perform: (elements, appState, _, app) => {
app.imageCache.clear();
return {
elements: elements.map(
(element) => newElementWith(element, { isDeleted: true })
),
appState: {
...getDefaultAppState(),
files: {},
theme: appState.theme,
penMode: appState.penMode,
penDetected: appState.penDetected,
exportBackground: appState.exportBackground,
exportEmbedScene: appState.exportEmbedScene,
gridSize: appState.gridSize,
gridStep: appState.gridStep,
gridModeEnabled: appState.gridModeEnabled,
stats: appState.stats,
pasteDialog: appState.pasteDialog,
activeTool: appState.activeTool.type === "image" ? { ...appState.activeTool, type: "selection" } : appState.activeTool
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
});
var actionZoomIn = register({
name: "zoomIn",
label: "buttons.zoomIn",
viewMode: true,
icon: ZoomInIcon,
trackEvent: { category: "canvas" },
perform: (_elements, appState, _, app) => {
return {
appState: {
...appState,
...getStateForZoom(
{
viewportX: appState.width / 2 + appState.offsetLeft,
viewportY: appState.height / 2 + appState.offsetTop,
nextZoom: getNormalizedZoom(appState.zoom.value + ZOOM_STEP)
},
appState
),
userToFollow: null
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ updateData, appState }) => /* @__PURE__ */ jsx36(
ToolButton,
{
type: "button",
className: "zoom-in-button zoom-button",
icon: ZoomInIcon,
title: `${t("buttons.zoomIn")} \u2014 ${getShortcutKey("CtrlOrCmd++")}`,
"aria-label": t("buttons.zoomIn"),
disabled: appState.zoom.value >= MAX_ZOOM,
onClick: () => {
updateData(null);
}
}
),
keyTest: (event) => (event.code === CODES.EQUAL || event.code === CODES.NUM_ADD) && (event[KEYS.CTRL_OR_CMD] || event.shiftKey)
});
var actionZoomOut = register({
name: "zoomOut",
label: "buttons.zoomOut",
icon: ZoomOutIcon,
viewMode: true,
trackEvent: { category: "canvas" },
perform: (_elements, appState, _, app) => {
return {
appState: {
...appState,
...getStateForZoom(
{
viewportX: appState.width / 2 + appState.offsetLeft,
viewportY: appState.height / 2 + appState.offsetTop,
nextZoom: getNormalizedZoom(appState.zoom.value - ZOOM_STEP)
},
appState
),
userToFollow: null
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ updateData, appState }) => /* @__PURE__ */ jsx36(
ToolButton,
{
type: "button",
className: "zoom-out-button zoom-button",
icon: ZoomOutIcon,
title: `${t("buttons.zoomOut")} \u2014 ${getShortcutKey("CtrlOrCmd+-")}`,
"aria-label": t("buttons.zoomOut"),
disabled: appState.zoom.value <= MIN_ZOOM,
onClick: () => {
updateData(null);
}
}
),
keyTest: (event) => (event.code === CODES.MINUS || event.code === CODES.NUM_SUBTRACT) && (event[KEYS.CTRL_OR_CMD] || event.shiftKey)
});
var actionResetZoom = register({
name: "resetZoom",
label: "buttons.resetZoom",
icon: ZoomResetIcon,
viewMode: true,
trackEvent: { category: "canvas" },
perform: (_elements, appState, _, app) => {
return {
appState: {
...appState,
...getStateForZoom(
{
viewportX: appState.width / 2 + appState.offsetLeft,
viewportY: appState.height / 2 + appState.offsetTop,
nextZoom: getNormalizedZoom(1)
},
appState
),
userToFollow: null
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ updateData, appState }) => /* @__PURE__ */ jsx36(Tooltip, { label: t("buttons.resetZoom"), style: { height: "100%" }, children: /* @__PURE__ */ jsxs21(
ToolButton,
{
type: "button",
className: "reset-zoom-button zoom-button",
title: t("buttons.resetZoom"),
"aria-label": t("buttons.resetZoom"),
onClick: () => {
updateData(null);
},
children: [
(appState.zoom.value * 100).toFixed(0),
"%"
]
}
) }),
keyTest: (event) => (event.code === CODES.ZERO || event.code === CODES.NUM_ZERO) && (event[KEYS.CTRL_OR_CMD] || event.shiftKey)
});
var zoomValueToFitBoundsOnViewport = (bounds, viewportDimensions, viewportZoomFactor = 1) => {
const [x1, y1, x2, y2] = bounds;
const commonBoundsWidth = x2 - x1;
const zoomValueForWidth = viewportDimensions.width / commonBoundsWidth;
const commonBoundsHeight = y2 - y1;
const zoomValueForHeight = viewportDimensions.height / commonBoundsHeight;
const smallestZoomValue = Math.min(zoomValueForWidth, zoomValueForHeight);
const adjustedZoomValue = smallestZoomValue * clamp(viewportZoomFactor, 0.1, 1);
return Math.min(adjustedZoomValue, 1);
};
var zoomToFitBounds = ({
bounds,
appState,
canvasOffsets,
fitToViewport = false,
viewportZoomFactor = 1,
minZoom = -Infinity,
maxZoom = Infinity
}) => {
viewportZoomFactor = clamp(viewportZoomFactor, MIN_ZOOM, MAX_ZOOM);
const [x1, y1, x2, y2] = bounds;
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
const canvasOffsetLeft = canvasOffsets?.left ?? 0;
const canvasOffsetTop = canvasOffsets?.top ?? 0;
const canvasOffsetRight = canvasOffsets?.right ?? 0;
const canvasOffsetBottom = canvasOffsets?.bottom ?? 0;
const effectiveCanvasWidth = appState.width - canvasOffsetLeft - canvasOffsetRight;
const effectiveCanvasHeight = appState.height - canvasOffsetTop - canvasOffsetBottom;
let adjustedZoomValue;
if (fitToViewport) {
const commonBoundsWidth = x2 - x1;
const commonBoundsHeight = y2 - y1;
adjustedZoomValue = Math.min(
effectiveCanvasWidth / commonBoundsWidth,
effectiveCanvasHeight / commonBoundsHeight
) * viewportZoomFactor;
} else {
adjustedZoomValue = zoomValueToFitBoundsOnViewport(
bounds,
{
width: effectiveCanvasWidth,
height: effectiveCanvasHeight
},
viewportZoomFactor
);
}
const newZoomValue = getNormalizedZoom(
clamp(roundToStep(adjustedZoomValue, ZOOM_STEP, "floor"), minZoom, maxZoom)
);
const centerScroll = centerScrollOn({
scenePoint: { x: centerX, y: centerY },
viewportDimensions: {
width: appState.width,
height: appState.height
},
offsets: canvasOffsets,
zoom: { value: newZoomValue }
});
return {
appState: {
...appState,
scrollX: centerScroll.scrollX,
scrollY: centerScroll.scrollY,
zoom: { value: newZoomValue }
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
};
var zoomToFit = ({
canvasOffsets,
targetElements,
appState,
fitToViewport,
viewportZoomFactor,
minZoom,
maxZoom
}) => {
const commonBounds = getCommonBounds(getNonDeletedElements(targetElements));
return zoomToFitBounds({
canvasOffsets,
bounds: commonBounds,
appState,
fitToViewport,
viewportZoomFactor,
minZoom,
maxZoom
});
};
var actionZoomToFitSelectionInViewport = register({
name: "zoomToFitSelectionInViewport",
label: "labels.zoomToFitViewport",
icon: zoomAreaIcon,
trackEvent: { category: "canvas" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements,
appState: {
...appState,
userToFollow: null
},
fitToViewport: false,
canvasOffsets: app.getEditorUIOffsets()
});
},
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
// TBD on how proceed
keyTest: (event) => event.code === CODES.TWO && event.shiftKey && !event.altKey && !event[KEYS.CTRL_OR_CMD]
});
var actionZoomToFitSelection = register({
name: "zoomToFitSelection",
label: "helpDialog.zoomToSelection",
icon: zoomAreaIcon,
trackEvent: { category: "canvas" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements,
appState: {
...appState,
userToFollow: null
},
fitToViewport: true,
canvasOffsets: app.getEditorUIOffsets()
});
},
// NOTE this action should use shift-2 per figma, alas
keyTest: (event) => event.code === CODES.THREE && event.shiftKey && !event.altKey && !event[KEYS.CTRL_OR_CMD]
});
var actionZoomToFit = register({
name: "zoomToFit",
label: "helpDialog.zoomToFit",
icon: zoomAreaIcon,
viewMode: true,
trackEvent: { category: "canvas" },
perform: (elements, appState, _, app) => zoomToFit({
targetElements: elements,
appState: {
...appState,
userToFollow: null
},
fitToViewport: false,
canvasOffsets: app.getEditorUIOffsets()
}),
keyTest: (event) => event.code === CODES.ONE && event.shiftKey && !event.altKey && !event[KEYS.CTRL_OR_CMD]
});
var actionToggleTheme = register({
name: "toggleTheme",
label: (_, appState) => {
return appState.theme === THEME.DARK ? "buttons.lightMode" : "buttons.darkMode";
},
keywords: ["toggle", "dark", "light", "mode", "theme"],
icon: (appState) => appState.theme === THEME.LIGHT ? MoonIcon : SunIcon,
viewMode: true,
trackEvent: { category: "canvas" },
perform: (_, appState, value) => {
return {
appState: {
...appState,
theme: value || (appState.theme === THEME.LIGHT ? THEME.DARK : THEME.LIGHT)
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
keyTest: (event) => event.altKey && event.shiftKey && event.code === CODES.D,
predicate: (elements, appState, props, app) => {
return !!app.props.UIOptions.canvasActions.toggleTheme;
}
});
var actionToggleEraserTool = register({
name: "toggleEraserTool",
label: "toolBar.eraser",
trackEvent: { category: "toolbar" },
perform: (elements, appState) => {
let activeTool;
if (isEraserActive(appState)) {
activeTool = updateActiveTool(appState, {
...appState.activeTool.lastActiveTool || {
type: "selection"
},
lastActiveToolBeforeEraser: null
});
} else {
activeTool = updateActiveTool(appState, {
type: "eraser",
lastActiveToolBeforeEraser: appState.activeTool
});
}
return {
appState: {
...appState,
selectedElementIds: {},
selectedGroupIds: {},
activeEmbeddable: null,
activeTool
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event.key === KEYS.E
});
var actionToggleHandTool = register({
name: "toggleHandTool",
label: "toolBar.hand",
paletteName: "Toggle hand tool",
trackEvent: { category: "toolbar" },
icon: handIcon,
viewMode: false,
perform: (elements, appState, _, app) => {
let activeTool;
if (isHandToolActive(appState)) {
activeTool = updateActiveTool(appState, {
...appState.activeTool.lastActiveTool || {
type: "selection"
},
lastActiveToolBeforeEraser: null
});
} else {
activeTool = updateActiveTool(appState, {
type: "hand",
lastActiveToolBeforeEraser: appState.activeTool
});
setCursor(app.interactiveCanvas, CURSOR_TYPE.GRAB);
}
return {
appState: {
...appState,
selectedElementIds: {},
selectedGroupIds: {},
activeEmbeddable: null,
activeTool
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => !event.altKey && !event[KEYS.CTRL_OR_CMD] && event.key === KEYS.H
});
// actions/actionFinalize.tsx
import { jsx as jsx37 } from "react/jsx-runtime";
var actionFinalize = register({
name: "finalize",
label: "",
trackEvent: false,
perform: (elements, appState, _, app) => {
const { interactiveCanvas, focusContainer, scene } = app;
const elementsMap = scene.getNonDeletedElementsMap();
if (appState.editingLinearElement) {
const { elementId, startBindingElement, endBindingElement } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (element) {
if (isBindingElement(element)) {
bindOrUnbindLinearElement(
element,
startBindingElement,
endBindingElement,
elementsMap,
scene
);
}
return {
elements: element.points.length < 2 || isInvisiblySmallElement(element) ? elements.filter((el) => el.id !== element.id) : void 0,
appState: {
...appState,
cursorButton: "up",
editingLinearElement: null
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
}
let newElements = elements;
const pendingImageElement = appState.pendingImageElementId && scene.getElement(appState.pendingImageElementId);
if (pendingImageElement) {
mutateElement(pendingImageElement, { isDeleted: true }, false);
}
if (window.document.activeElement instanceof HTMLElement) {
focusContainer();
}
const multiPointElement = appState.multiElement ? appState.multiElement : appState.newElement?.type === "freedraw" ? appState.newElement : null;
if (multiPointElement) {
if (multiPointElement.type !== "freedraw" && appState.lastPointerDownWith !== "touch") {
const { points, lastCommittedPoint } = multiPointElement;
if (!lastCommittedPoint || points[points.length - 1] !== lastCommittedPoint) {
mutateElement(multiPointElement, {
points: multiPointElement.points.slice(0, -1)
});
}
}
if (isInvisiblySmallElement(multiPointElement)) {
newElements = newElements.filter(
(el) => el.id !== multiPointElement.id
);
}
const isLoop = isPathALoop(multiPointElement.points, appState.zoom.value);
if (multiPointElement.type === "line" || multiPointElement.type === "freedraw") {
if (isLoop) {
const linePoints = multiPointElement.points;
const firstPoint = linePoints[0];
mutateElement(multiPointElement, {
points: linePoints.map(
(p, index) => index === linePoints.length - 1 ? pointFrom(firstPoint[0], firstPoint[1]) : p
)
});
}
}
if (isBindingElement(multiPointElement) && !isLoop && multiPointElement.points.length > 1) {
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
multiPointElement,
-1,
arrayToMap(elements)
);
maybeBindLinearElement(
multiPointElement,
appState,
{ x, y },
elementsMap,
elements
);
}
}
if (!appState.activeTool.locked && appState.activeTool.type !== "freedraw" || !multiPointElement) {
resetCursor(interactiveCanvas);
}
let activeTool;
if (appState.activeTool.type === "eraser") {
activeTool = updateActiveTool(appState, {
...appState.activeTool.lastActiveTool || {
type: "selection"
},
lastActiveToolBeforeEraser: null
});
} else {
activeTool = updateActiveTool(appState, {
type: "selection"
});
}
return {
elements: newElements,
appState: {
...appState,
cursorButton: "up",
activeTool: (appState.activeTool.locked || appState.activeTool.type === "freedraw") && multiPointElement ? appState.activeTool : activeTool,
activeEmbeddable: null,
newElement: null,
selectionElement: null,
multiElement: null,
editingTextElement: null,
startBoundElement: null,
suggestedBindings: [],
selectedElementIds: multiPointElement && !appState.activeTool.locked && appState.activeTool.type !== "freedraw" ? {
...appState.selectedElementIds,
[multiPointElement.id]: true
} : appState.selectedElementIds,
// To select the linear element when user has finished mutipoint editing
selectedLinearElement: multiPointElement && isLinearElement(multiPointElement) ? new LinearElementEditor(multiPointElement) : appState.selectedLinearElement,
pendingImageElementId: null
},
// TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event, appState) => event.key === KEYS.ESCAPE && (appState.editingLinearElement !== null || !appState.newElement && appState.multiElement === null) || (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && appState.multiElement !== null,
PanelComponent: ({ appState, updateData, data }) => /* @__PURE__ */ jsx37(
ToolButton,
{
type: "button",
icon: done,
title: t("buttons.done"),
"aria-label": t("buttons.done"),
onClick: updateData,
visible: appState.multiElement != null,
size: data?.size || "medium",
style: { pointerEvents: "all" }
}
)
});
// components/ProjectName.tsx
import { useState as useState9 } from "react";
import { jsx as jsx38, jsxs as jsxs22 } from "react/jsx-runtime";
var ProjectName = (props) => {
const { id } = useExcalidrawContainer();
const [fileName, setFileName] = useState9(props.value);
const handleBlur = (event) => {
if (!props.ignoreFocus) {
focusNearestParent(event.target);
}
const value = event.target.value;
if (value !== props.value) {
props.onChange(value);
}
};
const handleKeyDown = (event) => {
if (event.key === KEYS.ENTER) {
event.preventDefault();
if (event.nativeEvent.isComposing || event.keyCode === 229) {
return;
}
event.currentTarget.blur();
}
};
return /* @__PURE__ */ jsxs22("div", { className: "ProjectName", children: [
/* @__PURE__ */ jsx38("label", { className: "ProjectName-label", htmlFor: "filename", children: `${props.label}:` }),
/* @__PURE__ */ jsx38(
"input",
{
type: "text",
className: "TextInput",
onBlur: handleBlur,
onKeyDown: handleKeyDown,
id: `${id}-filename`,
value: fileName,
onChange: (event) => setFileName(event.target.value)
}
)
] });
};
// components/DarkModeToggle.tsx
import { jsx as jsx39 } from "react/jsx-runtime";
var DarkModeToggle = (props) => {
const title = props.title || (props.value === THEME.DARK ? t("buttons.lightMode") : t("buttons.darkMode"));
return /* @__PURE__ */ jsx39(
ToolButton,
{
type: "icon",
icon: props.value === THEME.LIGHT ? ICONS.MOON : ICONS.SUN,
title,
"aria-label": title,
onClick: () => props.onChange(props.value === THEME.DARK ? THEME.LIGHT : THEME.DARK),
"data-testid": "toggle-dark-mode"
}
);
};
var ICONS = {
SUN: /* @__PURE__ */ jsx39("svg", { width: "512", height: "512", className: "rtl-mirror", viewBox: "0 0 512 512", children: /* @__PURE__ */ jsx39(
"path",
{
fill: "currentColor",
d: "M256 160c-52.9 0-96 43.1-96 96s43.1 96 96 96 96-43.1 96-96-43.1-96-96-96zm246.4 80.5l-94.7-47.3 33.5-100.4c4.5-13.6-8.4-26.5-21.9-21.9l-100.4 33.5-47.4-94.8c-6.4-12.8-24.6-12.8-31 0l-47.3 94.7L92.7 70.8c-13.6-4.5-26.5 8.4-21.9 21.9l33.5 100.4-94.7 47.4c-12.8 6.4-12.8 24.6 0 31l94.7 47.3-33.5 100.5c-4.5 13.6 8.4 26.5 21.9 21.9l100.4-33.5 47.3 94.7c6.4 12.8 24.6 12.8 31 0l47.3-94.7 100.4 33.5c13.6 4.5 26.5-8.4 21.9-21.9l-33.5-100.4 94.7-47.3c13-6.5 13-24.7.2-31.1zm-155.9 106c-49.9 49.9-131.1 49.9-181 0-49.9-49.9-49.9-131.1 0-181 49.9-49.9 131.1-49.9 181 0 49.9 49.9 49.9 131.1 0 181z"
}
) }),
MOON: /* @__PURE__ */ jsx39("svg", { width: "512", height: "512", className: "rtl-mirror", viewBox: "0 0 512 512", children: /* @__PURE__ */ jsx39(
"path",
{
fill: "currentColor",
d: "M283.211 512c78.962 0 151.079-35.925 198.857-94.792 7.068-8.708-.639-21.43-11.562-19.35-124.203 23.654-238.262-71.576-238.262-196.954 0-72.222 38.662-138.635 101.498-174.394 9.686-5.512 7.25-20.197-3.756-22.23A258.156 258.156 0 0 0 283.211 0c-141.309 0-256 114.511-256 256 0 141.309 114.511 256 256 256z"
}
) })
};
// data/index.ts
var prepareElementsForExport = (elements, { selectedElementIds }, exportSelectionOnly) => {
elements = getNonDeletedElements(elements);
const isExportingSelection = exportSelectionOnly && isSomeElementSelected(elements, { selectedElementIds });
let exportingFrame = null;
let exportedElements = isExportingSelection ? getSelectedElements(
elements,
{ selectedElementIds },
{
includeBoundTextElement: true
}
) : elements;
if (isExportingSelection) {
if (exportedElements.length === 1 && isFrameLikeElement(exportedElements[0])) {
exportingFrame = exportedElements[0];
exportedElements = getElementsOverlappingFrame(elements, exportingFrame);
} else if (exportedElements.length > 1) {
exportedElements = getSelectedElements(
elements,
{ selectedElementIds },
{
includeBoundTextElement: true,
includeElementsInFrames: true
}
);
}
}
return {
exportingFrame,
exportedElements: cloneJSON(exportedElements)
};
};
var exportCanvas = async (type, elements, appState, files, {
exportBackground,
exportPadding = DEFAULT_EXPORT_PADDING,
viewBackgroundColor,
name = appState.name || DEFAULT_FILENAME,
fileHandle = null,
exportingFrame = null
}) => {
if (elements.length === 0) {
throw new Error(t("alerts.cannotExportEmptyCanvas"));
}
if (type === "svg" || type === "clipboard-svg") {
const svgPromise = exportToSvg(
elements,
{
exportBackground,
exportWithDarkMode: appState.exportWithDarkMode,
viewBackgroundColor,
exportPadding,
exportScale: appState.exportScale,
exportEmbedScene: appState.exportEmbedScene && type === "svg"
},
files,
{ exportingFrame }
);
if (type === "svg") {
return fileSave(
svgPromise.then((svg) => {
return new Blob([svg.outerHTML], { type: MIME_TYPES.svg });
}),
{
description: "Export to SVG",
name,
extension: appState.exportEmbedScene ? "excalidraw.svg" : "svg",
mimeTypes: [IMAGE_MIME_TYPES.svg],
fileHandle
}
);
} else if (type === "clipboard-svg") {
const svg = await svgPromise.then((svg2) => svg2.outerHTML);
try {
await copyTextToSystemClipboard(svg);
} catch (e) {
throw new Error(t("errors.copyToSystemClipboardFailed"));
}
return;
}
}
const tempCanvas = exportToCanvas(elements, appState, files, {
exportBackground,
viewBackgroundColor,
exportPadding,
exportingFrame
});
if (type === "png") {
let blob = canvasToBlob(tempCanvas);
if (appState.exportEmbedScene) {
blob = blob.then(
(blob2) => import("./data/image-WB3WPX2D.js").then(
({ encodePngMetadata }) => encodePngMetadata({
blob: blob2,
metadata: serializeAsJSON(elements, appState, files, "local")
})
)
);
}
return fileSave(blob, {
description: "Export to PNG",
name,
extension: appState.exportEmbedScene ? "excalidraw.png" : "png",
mimeTypes: [IMAGE_MIME_TYPES.png],
fileHandle
});
} else if (type === "clipboard") {
try {
const blob = canvasToBlob(tempCanvas);
await copyBlobToClipboardAsPng(blob);
} catch (error) {
console.warn(error);
if (error.name === "CANVAS_POSSIBLY_TOO_BIG") {
throw new Error(t("canvasError.canvasTooBig"));
}
if (isFirefox && error.name === "TypeError") {
throw new Error(
`${t("alerts.couldNotCopyToClipboard")}
${t(
"hints.firefox_clipboard_write"
)}`
);
} else {
throw new Error(t("alerts.couldNotCopyToClipboard"));
}
}
} else {
throw new Error("Unsupported export type");
}
};
// data/resave.ts
var resaveAsImageWithScene = async (elements, appState, files, name) => {
const { exportBackground, viewBackgroundColor, fileHandle } = appState;
const fileHandleType = getFileHandleType(fileHandle);
if (!fileHandle || !isImageFileHandleType(fileHandleType)) {
throw new Error(
"fileHandle should exist and should be of type svg or png when resaving"
);
}
appState = {
...appState,
exportEmbedScene: true
};
const { exportedElements, exportingFrame } = prepareElementsForExport(
elements,
appState,
false
);
await exportCanvas(fileHandleType, exportedElements, appState, files, {
exportBackground,
viewBackgroundColor,
name,
fileHandle,
exportingFrame
});
return { fileHandle };
};
// components/CheckboxItem.tsx
import clsx16 from "clsx";
import { jsx as jsx40, jsxs as jsxs23 } from "react/jsx-runtime";
var CheckboxItem = ({ children, checked, onChange, className }) => {
return /* @__PURE__ */ jsxs23(
"div",
{
className: clsx16("Checkbox", className, { "is-checked": checked }),
onClick: (event) => {
onChange(!checked, event);
event.currentTarget.querySelector(
".Checkbox-box"
).focus();
},
children: [
/* @__PURE__ */ jsx40(
"button",
{
type: "button",
className: "Checkbox-box",
role: "checkbox",
"aria-checked": checked,
children: checkIcon
}
),
/* @__PURE__ */ jsx40("div", { className: "Checkbox-label", children })
]
}
);
};
// actions/actionExport.tsx
import { Fragment as Fragment5, jsx as jsx41, jsxs as jsxs24 } from "react/jsx-runtime";
var actionChangeProjectName = register({
name: "changeProjectName",
label: "labels.fileTitle",
trackEvent: false,
perform: (_elements, appState, value) => {
return {
appState: { ...appState, name: value },
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ appState, updateData, appProps, data, app }) => /* @__PURE__ */ jsx41(
ProjectName,
{
label: t("labels.fileTitle"),
value: app.getName(),
onChange: (name) => updateData(name),
ignoreFocus: data?.ignoreFocus ?? false
}
)
});
var actionChangeExportScale = register({
name: "changeExportScale",
label: "imageExportDialog.scale",
trackEvent: { category: "export", action: "scale" },
perform: (_elements, appState, value) => {
return {
appState: { ...appState, exportScale: value },
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ elements: allElements, appState, updateData }) => {
const elements = getNonDeletedElements(allElements);
const exportSelected = isSomeElementSelected(elements, appState);
const exportedElements = exportSelected ? getSelectedElements(elements, appState) : elements;
return /* @__PURE__ */ jsx41(Fragment5, { children: EXPORT_SCALES.map((s) => {
const [width, height] = getExportSize(
exportedElements,
DEFAULT_EXPORT_PADDING,
s
);
const scaleButtonTitle = `${t(
"imageExportDialog.label.scale"
)} ${s}x (${width}x${height})`;
return /* @__PURE__ */ jsx41(
ToolButton,
{
size: "small",
type: "radio",
icon: `${s}x`,
name: "export-canvas-scale",
title: scaleButtonTitle,
"aria-label": scaleButtonTitle,
id: "export-canvas-scale",
checked: s === appState.exportScale,
onChange: () => updateData(s)
},
s
);
}) });
}
});
var actionChangeExportBackground = register({
name: "changeExportBackground",
label: "imageExportDialog.label.withBackground",
trackEvent: { category: "export", action: "toggleBackground" },
perform: (_elements, appState, value) => {
return {
appState: { ...appState, exportBackground: value },
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ appState, updateData }) => /* @__PURE__ */ jsx41(
CheckboxItem,
{
checked: appState.exportBackground,
onChange: (checked) => updateData(checked),
children: t("imageExportDialog.label.withBackground")
}
)
});
var actionChangeExportEmbedScene = register({
name: "changeExportEmbedScene",
label: "imageExportDialog.tooltip.embedScene",
trackEvent: { category: "export", action: "embedScene" },
perform: (_elements, appState, value) => {
return {
appState: { ...appState, exportEmbedScene: value },
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ appState, updateData }) => /* @__PURE__ */ jsxs24(
CheckboxItem,
{
checked: appState.exportEmbedScene,
onChange: (checked) => updateData(checked),
children: [
t("imageExportDialog.label.embedScene"),
/* @__PURE__ */ jsx41(Tooltip, { label: t("imageExportDialog.tooltip.embedScene"), long: true, children: /* @__PURE__ */ jsx41("div", { className: "excalidraw-tooltip-icon", children: questionCircle }) })
]
}
)
});
var actionSaveToActiveFile = register({
name: "saveToActiveFile",
label: "buttons.save",
icon: ExportIcon,
trackEvent: { category: "export" },
predicate: (elements, appState, props, app) => {
return !!app.props.UIOptions.canvasActions.saveToActiveFile && !!appState.fileHandle && !appState.viewModeEnabled;
},
perform: async (elements, appState, value, app) => {
const fileHandleExists = !!appState.fileHandle;
try {
const { fileHandle } = isImageFileHandle(appState.fileHandle) ? await resaveAsImageWithScene(
elements,
appState,
app.files,
app.getName()
) : await saveAsJSON(elements, appState, app.files, app.getName());
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
appState: {
...appState,
fileHandle,
toast: fileHandleExists ? {
message: fileHandle?.name ? t("toast.fileSavedToFilename").replace(
"{filename}",
`"${fileHandle.name}"`
) : t("toast.fileSaved")
} : null
}
};
} catch (error) {
if (error?.name !== "AbortError") {
console.error(error);
} else {
console.warn(error);
}
return { captureUpdate: CaptureUpdateAction.EVENTUALLY };
}
},
keyTest: (event) => event.key === KEYS.S && event[KEYS.CTRL_OR_CMD] && !event.shiftKey
});
var actionSaveFileToDisk = register({
name: "saveFileToDisk",
label: "exportDialog.disk_title",
icon: ExportIcon,
viewMode: true,
trackEvent: { category: "export" },
perform: async (elements, appState, value, app) => {
try {
const { fileHandle } = await saveAsJSON(
elements,
{
...appState,
fileHandle: null
},
app.files,
app.getName()
);
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
appState: {
...appState,
openDialog: null,
fileHandle,
toast: { message: t("toast.fileSaved") }
}
};
} catch (error) {
if (error?.name !== "AbortError") {
console.error(error);
} else {
console.warn(error);
}
return { captureUpdate: CaptureUpdateAction.EVENTUALLY };
}
},
keyTest: (event) => event.key === KEYS.S && event.shiftKey && event[KEYS.CTRL_OR_CMD],
PanelComponent: ({ updateData }) => /* @__PURE__ */ jsx41(
ToolButton,
{
type: "button",
icon: saveAs,
title: t("buttons.saveAs"),
"aria-label": t("buttons.saveAs"),
showAriaLabel: useDevice().editor.isMobile,
hidden: !nativeFileSystemSupported,
onClick: () => updateData(null),
"data-testid": "save-as-button"
}
)
});
var actionLoadScene = register({
name: "loadScene",
label: "buttons.load",
trackEvent: { category: "export" },
predicate: (elements, appState, props, app) => {
return !!app.props.UIOptions.canvasActions.loadScene && !appState.viewModeEnabled;
},
perform: async (elements, appState, _, app) => {
try {
const {
elements: loadedElements,
appState: loadedAppState,
files
} = await loadFromJSON(appState, elements);
return {
elements: loadedElements,
appState: loadedAppState,
files,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
} catch (error) {
if (error?.name === "AbortError") {
console.warn(error);
return false;
}
return {
elements,
appState: { ...appState, errorMessage: error.message },
files: app.files,
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.O
});
var actionExportWithDarkMode = register({
name: "exportWithDarkMode",
label: "imageExportDialog.label.darkMode",
trackEvent: { category: "export", action: "toggleTheme" },
perform: (_elements, appState, value) => {
return {
appState: { ...appState, exportWithDarkMode: value },
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ appState, updateData }) => /* @__PURE__ */ jsx41(
"div",
{
style: {
display: "flex",
justifyContent: "flex-end",
marginTop: "-45px",
marginBottom: "10px"
},
children: /* @__PURE__ */ jsx41(
DarkModeToggle,
{
value: appState.exportWithDarkMode ? THEME.DARK : THEME.LIGHT,
onChange: (theme) => {
updateData(theme === THEME.DARK);
},
title: t("imageExportDialog.label.darkMode")
}
)
}
)
});
// actions/actionStyles.ts
var copiedStyles = "{}";
var actionCopyStyles = register({
name: "copyStyles",
label: "labels.copyStyles",
icon: paintIcon,
trackEvent: { category: "element" },
perform: (elements, appState, formData, app) => {
const elementsCopied = [];
const element = elements.find((el) => appState.selectedElementIds[el.id]);
elementsCopied.push(element);
if (element && hasBoundTextElement(element)) {
const boundTextElement = getBoundTextElement(
element,
app.scene.getNonDeletedElementsMap()
);
elementsCopied.push(boundTextElement);
}
if (element) {
copiedStyles = JSON.stringify(elementsCopied);
}
return {
appState: {
...appState,
toast: { message: t("toast.copyStyles") }
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.C
});
var actionPasteStyles = register({
name: "pasteStyles",
label: "labels.pasteStyles",
icon: paintIcon,
trackEvent: { category: "element" },
perform: (elements, appState, formData, app) => {
const elementsCopied = JSON.parse(copiedStyles);
const pastedElement = elementsCopied[0];
const boundTextElement = elementsCopied[1];
if (!isExcalidrawElement(pastedElement)) {
return { elements, captureUpdate: CaptureUpdateAction.EVENTUALLY };
}
const selectedElements = getSelectedElements(elements, appState, {
includeBoundTextElement: true
});
const selectedElementIds = selectedElements.map((element) => element.id);
return {
elements: elements.map((element) => {
if (selectedElementIds.includes(element.id)) {
let elementStylesToCopyFrom = pastedElement;
if (isTextElement(element) && element.containerId) {
elementStylesToCopyFrom = boundTextElement;
}
if (!elementStylesToCopyFrom) {
return element;
}
let newElement2 = newElementWith(element, {
backgroundColor: elementStylesToCopyFrom?.backgroundColor,
strokeWidth: elementStylesToCopyFrom?.strokeWidth,
strokeColor: elementStylesToCopyFrom?.strokeColor,
strokeStyle: elementStylesToCopyFrom?.strokeStyle,
fillStyle: elementStylesToCopyFrom?.fillStyle,
opacity: elementStylesToCopyFrom?.opacity,
roughness: elementStylesToCopyFrom?.roughness,
roundness: elementStylesToCopyFrom.roundness ? canApplyRoundnessTypeToElement(
elementStylesToCopyFrom.roundness.type,
element
) ? elementStylesToCopyFrom.roundness : getDefaultRoundnessTypeForElement(element) : null
});
if (isTextElement(newElement2)) {
const fontSize = elementStylesToCopyFrom.fontSize || DEFAULT_FONT_SIZE;
const fontFamily = elementStylesToCopyFrom.fontFamily || DEFAULT_FONT_FAMILY;
newElement2 = newElementWith(newElement2, {
fontSize,
fontFamily,
textAlign: elementStylesToCopyFrom.textAlign || DEFAULT_TEXT_ALIGN,
lineHeight: elementStylesToCopyFrom.lineHeight || getLineHeight(fontFamily)
});
let container = null;
if (newElement2.containerId) {
container = selectedElements.find(
(element2) => isTextElement(newElement2) && element2.id === newElement2.containerId
) || null;
}
redrawTextBoundingBox(
newElement2,
container,
app.scene.getNonDeletedElementsMap()
);
}
if (newElement2.type === "arrow" && isArrowElement(elementStylesToCopyFrom)) {
newElement2 = newElementWith(newElement2, {
startArrowhead: elementStylesToCopyFrom.startArrowhead,
endArrowhead: elementStylesToCopyFrom.endArrowhead
});
}
if (isFrameLikeElement(element)) {
newElement2 = newElementWith(newElement2, {
roundness: null,
backgroundColor: "transparent"
});
}
return newElement2;
}
return element;
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V
});
// actions/actionMenu.tsx
import { jsx as jsx42 } from "react/jsx-runtime";
var actionToggleCanvasMenu = register({
name: "toggleCanvasMenu",
label: "buttons.menu",
trackEvent: { category: "menu" },
perform: (_, appState) => ({
appState: {
...appState,
openMenu: appState.openMenu === "canvas" ? null : "canvas"
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
}),
PanelComponent: ({ appState, updateData }) => /* @__PURE__ */ jsx42(
ToolButton,
{
type: "button",
icon: HamburgerMenuIcon,
"aria-label": t("buttons.menu"),
onClick: updateData,
selected: appState.openMenu === "canvas"
}
)
});
var actionToggleEditMenu = register({
name: "toggleEditMenu",
label: "buttons.edit",
trackEvent: { category: "menu" },
perform: (_elements, appState) => ({
appState: {
...appState,
openMenu: appState.openMenu === "shape" ? null : "shape"
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
}),
PanelComponent: ({ elements, appState, updateData }) => /* @__PURE__ */ jsx42(
ToolButton,
{
visible: showSelectedShapeActions(
appState,
getNonDeletedElements(elements)
),
type: "button",
icon: palette,
"aria-label": t("buttons.edit"),
onClick: updateData,
selected: appState.openMenu === "shape"
}
)
});
var actionShortcuts = register({
name: "toggleShortcuts",
label: "welcomeScreen.defaults.helpHint",
icon: HelpIconThin,
viewMode: true,
trackEvent: { category: "menu", action: "toggleHelpDialog" },
perform: (_elements, appState, _, { focusContainer }) => {
if (appState.openDialog?.name === "help") {
focusContainer();
}
return {
appState: {
...appState,
openDialog: appState.openDialog?.name === "help" ? null : {
name: "help"
}
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
keyTest: (event) => event.key === KEYS.QUESTION_MARK
});
// actions/actionGroup.tsx
import { jsx as jsx43 } from "react/jsx-runtime";
var allElementsInSameGroup = (elements) => {
if (elements.length >= 2) {
const groupIds = elements[0].groupIds;
for (const groupId of groupIds) {
if (elements.reduce(
(acc, element) => acc && isElementInGroup(element, groupId),
true
)) {
return true;
}
}
}
return false;
};
var enableActionGroup = (elements, appState, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true
});
return selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements) && !frameAndChildrenSelectedTogether(selectedElements);
};
var actionGroup = register({
name: "group",
label: "labels.group",
icon: (appState) => /* @__PURE__ */ jsx43(GroupIcon, { theme: appState.theme }),
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const selectedElements = getRootElements(
app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true
})
);
if (selectedElements.length < 2) {
return {
appState,
elements,
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
const selectedGroupIds = getSelectedGroupIds(appState);
if (selectedGroupIds.length === 1) {
const selectedGroupId = selectedGroupIds[0];
const elementIdsInGroup = new Set(
getElementsInGroup(elements, selectedGroupId).map(
(element) => element.id
)
);
const selectedElementIds = new Set(
selectedElements.map((element) => element.id)
);
const combinedSet = /* @__PURE__ */ new Set([
...Array.from(elementIdsInGroup),
...Array.from(selectedElementIds)
]);
if (combinedSet.size === elementIdsInGroup.size) {
return {
appState,
elements,
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
}
let nextElements = [...elements];
const groupingElementsFromDifferentFrames = new Set(selectedElements.map((element) => element.frameId)).size > 1;
if (groupingElementsFromDifferentFrames) {
const frameElementsMap = groupByFrameLikes(selectedElements);
frameElementsMap.forEach((elementsInFrame, frameId) => {
removeElementsFromFrame(
elementsInFrame,
app.scene.getNonDeletedElementsMap()
);
});
}
const newGroupId = randomId();
const selectElementIds = arrayToMap(selectedElements);
nextElements = nextElements.map((element) => {
if (!selectElementIds.get(element.id)) {
return element;
}
return newElementWith(element, {
groupIds: addToGroup(
element.groupIds,
newGroupId,
appState.editingGroupId
)
});
});
const elementsInGroup = getElementsInGroup(nextElements, newGroupId);
const lastElementInGroup = elementsInGroup[elementsInGroup.length - 1];
const lastGroupElementIndex = nextElements.lastIndexOf(
lastElementInGroup
);
const elementsAfterGroup = nextElements.slice(lastGroupElementIndex + 1);
const elementsBeforeGroup = nextElements.slice(0, lastGroupElementIndex).filter(
(updatedElement) => !isElementInGroup(updatedElement, newGroupId)
);
const reorderedElements = syncMovedIndices(
[...elementsBeforeGroup, ...elementsInGroup, ...elementsAfterGroup],
arrayToMap(elementsInGroup)
);
return {
appState: {
...appState,
...selectGroup(
newGroupId,
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements)
)
},
elements: reorderedElements,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
predicate: (elements, appState, _, app) => enableActionGroup(elements, appState, app),
keyTest: (event) => !event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
PanelComponent: ({ elements, appState, updateData, app }) => /* @__PURE__ */ jsx43(
ToolButton,
{
hidden: !enableActionGroup(elements, appState, app),
type: "button",
icon: /* @__PURE__ */ jsx43(GroupIcon, { theme: appState.theme }),
onClick: () => updateData(null),
title: `${t("labels.group")} \u2014 ${getShortcutKey("CtrlOrCmd+G")}`,
"aria-label": t("labels.group"),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
var actionUngroup = register({
name: "ungroup",
label: "labels.ungroup",
icon: (appState) => /* @__PURE__ */ jsx43(UngroupIcon, { theme: appState.theme }),
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const groupIds = getSelectedGroupIds(appState);
const elementsMap = arrayToMap(elements);
if (groupIds.length === 0) {
return {
appState,
elements,
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
let nextElements = [...elements];
const boundTextElementIds = [];
nextElements = nextElements.map((element) => {
if (isBoundToContainer(element)) {
boundTextElementIds.push(element.id);
}
const nextGroupIds = removeFromSelectedGroups(
element.groupIds,
appState.selectedGroupIds
);
if (nextGroupIds.length === element.groupIds.length) {
return element;
}
return newElementWith(element, {
groupIds: nextGroupIds
});
});
const updateAppState = selectGroupsForSelectedElements(
appState,
getNonDeletedElements(nextElements),
appState,
null
);
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElementFrameIds = new Set(
selectedElements.filter((element) => element.frameId).map((element) => element.frameId)
);
const targetFrames = getFrameLikeElements(elements).filter(
(frame) => selectedElementFrameIds.has(frame.id)
);
targetFrames.forEach((frame) => {
if (frame) {
nextElements = replaceAllElementsInFrame(
nextElements,
getElementsInResizingFrame(
nextElements,
frame,
appState,
elementsMap
),
frame,
app
);
}
});
updateAppState.selectedElementIds = Object.entries(
updateAppState.selectedElementIds
).reduce(
(acc, [id, selected]) => {
if (selected && !boundTextElementIds.includes(id)) {
acc[id] = true;
}
return acc;
},
{}
);
return {
appState: { ...appState, ...updateAppState },
elements: nextElements,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G.toUpperCase(),
predicate: (elements, appState) => getSelectedGroupIds(appState).length > 0,
PanelComponent: ({ elements, appState, updateData }) => /* @__PURE__ */ jsx43(
ToolButton,
{
type: "button",
hidden: getSelectedGroupIds(appState).length === 0,
icon: /* @__PURE__ */ jsx43(UngroupIcon, { theme: appState.theme }),
onClick: () => updateData(null),
title: `${t("labels.ungroup")} \u2014 ${getShortcutKey("CtrlOrCmd+Shift+G")}`,
"aria-label": t("labels.ungroup"),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
// renderer/roundRect.ts
var roundRect = (context, x, y, width, height, radius, strokeColor) => {
context.beginPath();
context.moveTo(x + radius, y);
context.lineTo(x + width - radius, y);
context.quadraticCurveTo(x + width, y, x + width, y + radius);
context.lineTo(x + width, y + height - radius);
context.quadraticCurveTo(
x + width,
y + height,
x + width - radius,
y + height
);
context.lineTo(x + radius, y + height);
context.quadraticCurveTo(x, y + height, x, y + height - radius);
context.lineTo(x, y + radius);
context.quadraticCurveTo(x, y, x + radius, y);
context.closePath();
context.fill();
if (strokeColor) {
context.strokeStyle = strokeColor;
}
context.stroke();
};
// clients.ts
function hashToInteger(id) {
let hash = 0;
if (id.length === 0) {
return hash;
}
for (let i = 0; i < id.length; i++) {
const char = id.charCodeAt(i);
hash = (hash << 5) - hash + char;
}
return hash;
}
var getClientColor = (socketId, collaborator) => {
const hash = Math.abs(hashToInteger(collaborator?.id || socketId));
const hue = hash % 37 * 10;
const saturation = 100;
const lightness = 83;
return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
};
var getNameInitial = (name) => {
const firstCodePoint = name?.trim()?.codePointAt(0);
return (firstCodePoint ? String.fromCodePoint(firstCodePoint) : "?").toUpperCase();
};
var renderRemoteCursors = ({
context,
renderConfig,
appState,
normalizedWidth,
normalizedHeight
}) => {
for (const [socketId, pointer] of renderConfig.remotePointerViewportCoords) {
let { x, y } = pointer;
const collaborator = appState.collaborators.get(socketId);
x -= appState.offsetLeft;
y -= appState.offsetTop;
const width = 11;
const height = 14;
const isOutOfBounds = x < 0 || x > normalizedWidth - width || y < 0 || y > normalizedHeight - height;
x = Math.max(x, 0);
x = Math.min(x, normalizedWidth - width);
y = Math.max(y, 0);
y = Math.min(y, normalizedHeight - height);
const background = getClientColor(socketId, collaborator);
context.save();
context.strokeStyle = background;
context.fillStyle = background;
const userState = renderConfig.remotePointerUserStates.get(socketId);
const isInactive = isOutOfBounds || userState === "idle" /* IDLE */ || userState === "away" /* AWAY */;
if (isInactive) {
context.globalAlpha = 0.3;
}
if (renderConfig.remotePointerButton.get(socketId) === "down") {
context.beginPath();
context.arc(x, y, 15, 0, 2 * Math.PI, false);
context.lineWidth = 3;
context.strokeStyle = "#ffffff88";
context.stroke();
context.closePath();
context.beginPath();
context.arc(x, y, 15, 0, 2 * Math.PI, false);
context.lineWidth = 1;
context.strokeStyle = background;
context.stroke();
context.closePath();
}
const IS_SPEAKING_COLOR = appState.theme === THEME.DARK ? "#2f6330" : COLOR_VOICE_CALL;
const isSpeaking = collaborator?.isSpeaking;
if (isSpeaking) {
context.fillStyle = IS_SPEAKING_COLOR;
context.strokeStyle = IS_SPEAKING_COLOR;
context.lineWidth = 10;
context.lineJoin = "round";
context.beginPath();
context.moveTo(x, y);
context.lineTo(x + 0, y + 14);
context.lineTo(x + 4, y + 9);
context.lineTo(x + 11, y + 8);
context.closePath();
context.stroke();
context.fill();
}
context.fillStyle = COLOR_WHITE;
context.strokeStyle = COLOR_WHITE;
context.lineWidth = 6;
context.lineJoin = "round";
context.beginPath();
context.moveTo(x, y);
context.lineTo(x + 0, y + 14);
context.lineTo(x + 4, y + 9);
context.lineTo(x + 11, y + 8);
context.closePath();
context.stroke();
context.fill();
context.fillStyle = background;
context.strokeStyle = background;
context.lineWidth = 2;
context.lineJoin = "round";
context.beginPath();
if (isInactive) {
context.moveTo(x - 1, y - 1);
context.lineTo(x - 1, y + 15);
context.lineTo(x + 5, y + 10);
context.lineTo(x + 12, y + 9);
context.closePath();
context.fill();
} else {
context.moveTo(x, y);
context.lineTo(x + 0, y + 14);
context.lineTo(x + 4, y + 9);
context.lineTo(x + 11, y + 8);
context.closePath();
context.fill();
context.stroke();
}
const username = renderConfig.remotePointerUsernames.get(socketId) || "";
if (!isOutOfBounds && username) {
context.font = "600 12px sans-serif";
const offsetX = (isSpeaking ? x + 0 : x) + width / 2;
const offsetY = (isSpeaking ? y + 0 : y) + height + 2;
const paddingHorizontal = 5;
const paddingVertical = 3;
const measure = context.measureText(username);
const measureHeight = measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent;
const finalHeight = Math.max(measureHeight, 12);
const boxX = offsetX - 1;
const boxY = offsetY - 1;
const boxWidth = measure.width + 2 + paddingHorizontal * 2 + 2;
const boxHeight = finalHeight + 2 + paddingVertical * 2 + 2;
if (context.roundRect) {
context.beginPath();
context.roundRect(boxX, boxY, boxWidth, boxHeight, 8);
context.fillStyle = background;
context.fill();
context.strokeStyle = COLOR_WHITE;
context.stroke();
if (isSpeaking) {
context.beginPath();
context.roundRect(boxX - 2, boxY - 2, boxWidth + 4, boxHeight + 4, 8);
context.strokeStyle = IS_SPEAKING_COLOR;
context.stroke();
}
} else {
roundRect(context, boxX, boxY, boxWidth, boxHeight, 8, COLOR_WHITE);
}
context.fillStyle = COLOR_CHARCOAL_BLACK;
context.fillText(
username,
offsetX + paddingHorizontal + 1,
offsetY + paddingVertical + measure.actualBoundingBoxAscent + Math.floor((finalHeight - measureHeight) / 2) + 2
);
if (isSpeaking) {
context.fillStyle = IS_SPEAKING_COLOR;
const barheight = 8;
const margin = 8;
const gap = 5;
context.fillRect(
boxX + boxWidth + margin,
boxY + (boxHeight / 2 - barheight / 2),
2,
barheight
);
context.fillRect(
boxX + boxWidth + margin + gap,
boxY + (boxHeight / 2 - barheight * 2 / 2),
2,
barheight * 2
);
context.fillRect(
boxX + boxWidth + margin + gap * 2,
boxY + (boxHeight / 2 - barheight / 2),
2,
barheight
);
}
}
context.restore();
context.closePath();
}
};
// components/Avatar.tsx
import { useState as useState10 } from "react";
import clsx17 from "clsx";
import { jsx as jsx44 } from "react/jsx-runtime";
var Avatar = ({
color,
onClick,
name,
src,
className
}) => {
const shortName = getNameInitial(name);
const [error, setError] = useState10(false);
const loadImg = !error && src;
const style = loadImg ? void 0 : { background: color };
return /* @__PURE__ */ jsx44("div", { className: clsx17("Avatar", className), style, onClick, children: loadImg ? /* @__PURE__ */ jsx44(
"img",
{
className: "Avatar-img",
src,
alt: shortName,
referrerPolicy: "no-referrer",
onError: () => setError(true)
}
) : shortName });
};
// actions/actionNavigate.tsx
import clsx18 from "clsx";
import { jsx as jsx45, jsxs as jsxs25 } from "react/jsx-runtime";
var actionGoToCollaborator = register({
name: "goToCollaborator",
label: "Go to a collaborator",
viewMode: true,
trackEvent: { category: "collab" },
perform: (_elements, appState, collaborator) => {
if (!collaborator.socketId || appState.userToFollow?.socketId === collaborator.socketId || collaborator.isCurrentUser) {
return {
appState: {
...appState,
userToFollow: null
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
return {
appState: {
...appState,
userToFollow: {
socketId: collaborator.socketId,
username: collaborator.username || ""
},
// Close mobile menu
openMenu: appState.openMenu === "canvas" ? null : appState.openMenu
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
PanelComponent: ({ updateData, data, appState }) => {
const { socketId, collaborator, withName, isBeingFollowed } = data;
const background = getClientColor(socketId, collaborator);
const statusClassNames = clsx18({
"is-followed": isBeingFollowed,
"is-current-user": collaborator.isCurrentUser === true,
"is-speaking": collaborator.isSpeaking,
"is-in-call": collaborator.isInCall,
"is-muted": collaborator.isMuted
});
const statusIconJSX = collaborator.isInCall ? collaborator.isSpeaking ? /* @__PURE__ */ jsxs25(
"div",
{
className: "UserList__collaborator-status-icon-speaking-indicator",
title: t("userList.hint.isSpeaking"),
children: [
/* @__PURE__ */ jsx45("div", {}),
/* @__PURE__ */ jsx45("div", {}),
/* @__PURE__ */ jsx45("div", {})
]
}
) : collaborator.isMuted ? /* @__PURE__ */ jsx45(
"div",
{
className: "UserList__collaborator-status-icon-microphone-muted",
title: t("userList.hint.micMuted"),
children: microphoneMutedIcon
}
) : /* @__PURE__ */ jsx45("div", { title: t("userList.hint.inCall"), children: microphoneIcon }) : null;
return withName ? /* @__PURE__ */ jsxs25(
"div",
{
className: `dropdown-menu-item dropdown-menu-item-base UserList__collaborator ${statusClassNames}`,
style: { [`--avatar-size`]: "1.5rem" },
onClick: () => updateData(collaborator),
children: [
/* @__PURE__ */ jsx45(
Avatar,
{
color: background,
onClick: () => {
},
name: collaborator.username || "",
src: collaborator.avatarUrl,
className: statusClassNames
}
),
/* @__PURE__ */ jsx45("div", { className: "UserList__collaborator-name", children: collaborator.username }),
/* @__PURE__ */ jsxs25("div", { className: "UserList__collaborator-status-icons", "aria-hidden": true, children: [
isBeingFollowed && /* @__PURE__ */ jsx45(
"div",
{
className: "UserList__collaborator-status-icon-is-followed",
title: t("userList.hint.followStatus"),
children: eyeIcon
}
),
statusIconJSX
] })
]
}
) : /* @__PURE__ */ jsxs25(
"div",
{
className: `UserList__collaborator UserList__collaborator--avatar-only ${statusClassNames}`,
children: [
/* @__PURE__ */ jsx45(
Avatar,
{
color: background,
onClick: () => {
updateData(collaborator);
},
name: collaborator.username || "",
src: collaborator.avatarUrl,
className: statusClassNames
}
),
statusIconJSX && /* @__PURE__ */ jsx45("div", { className: "UserList__collaborator-status-icon", children: statusIconJSX })
]
}
);
}
});
// actions/actionAddToLibrary.ts
var actionAddToLibrary = register({
name: "addToLibrary",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true
});
for (const type of LIBRARY_DISABLED_TYPES) {
if (selectedElements.some((element) => element.type === type)) {
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
appState: {
...appState,
errorMessage: t(`errors.libraryElementTypeError.${type}`)
}
};
}
}
return app.library.getLatestLibrary().then((items) => {
return app.library.setLibrary([
{
id: randomId(),
status: "unpublished",
elements: selectedElements.map(deepCopyElement),
created: Date.now()
},
...items
]);
}).then(() => {
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
appState: {
...appState,
toast: { message: t("toast.addedToLibrary") }
}
};
}).catch((error) => {
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
appState: {
...appState,
errorMessage: error.message
}
};
});
},
label: "labels.addToLibrary"
});
// align.ts
var alignElements = (selectedElements, elementsMap, alignment, scene) => {
const groups = getMaximumGroups(
selectedElements,
elementsMap
);
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
return groups.flatMap((group) => {
const translation = calculateTranslation(
group,
selectionBoundingBox,
alignment
);
return group.map((element) => {
const updatedEle = mutateElement(element, {
x: element.x + translation.x,
y: element.y + translation.y
});
updateBoundElements(element, scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: group
});
return updatedEle;
});
});
};
var calculateTranslation = (group, selectionBoundingBox, { axis, position }) => {
const groupBoundingBox = getCommonBoundingBox(group);
const [min, max] = axis === "x" ? ["minX", "maxX"] : ["minY", "maxY"];
const noTranslation = { x: 0, y: 0 };
if (position === "start") {
return {
...noTranslation,
[axis]: selectionBoundingBox[min] - groupBoundingBox[min]
};
} else if (position === "end") {
return {
...noTranslation,
[axis]: selectionBoundingBox[max] - groupBoundingBox[max]
};
}
return {
...noTranslation,
[axis]: (selectionBoundingBox[min] + selectionBoundingBox[max]) / 2 - (groupBoundingBox[min] + groupBoundingBox[max]) / 2
};
};
// actions/actionAlign.tsx
import { jsx as jsx46 } from "react/jsx-runtime";
var alignActionsPredicate = (appState, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
return selectedElements.length > 1 && // TODO enable aligning frames when implemented properly
!selectedElements.some((el) => isFrameLikeElement(el));
};
var alignSelectedElements = (elements, appState, app, alignment) => {
const selectedElements = app.scene.getSelectedElements(appState);
const elementsMap = arrayToMap(elements);
const updatedElements = alignElements(
selectedElements,
elementsMap,
alignment,
app.scene
);
const updatedElementsMap = arrayToMap(updatedElements);
return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element),
appState,
app
);
};
var actionAlignTop = register({
name: "alignTop",
label: "labels.alignTop",
icon: AlignTopIcon,
trackEvent: { category: "element" },
predicate: (elements, appState, appProps, app) => alignActionsPredicate(appState, app),
perform: (elements, appState, _, app) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
position: "start",
axis: "y"
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
PanelComponent: ({ elements, appState, updateData, app }) => /* @__PURE__ */ jsx46(
ToolButton,
{
hidden: !alignActionsPredicate(appState, app),
type: "button",
icon: AlignTopIcon,
onClick: () => updateData(null),
title: `${t("labels.alignTop")} \u2014 ${getShortcutKey(
"CtrlOrCmd+Shift+Up"
)}`,
"aria-label": t("labels.alignTop"),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
var actionAlignBottom = register({
name: "alignBottom",
label: "labels.alignBottom",
icon: AlignBottomIcon,
trackEvent: { category: "element" },
predicate: (elements, appState, appProps, app) => alignActionsPredicate(appState, app),
perform: (elements, appState, _, app) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
position: "end",
axis: "y"
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
PanelComponent: ({ elements, appState, updateData, app }) => /* @__PURE__ */ jsx46(
ToolButton,
{
hidden: !alignActionsPredicate(appState, app),
type: "button",
icon: AlignBottomIcon,
onClick: () => updateData(null),
title: `${t("labels.alignBottom")} \u2014 ${getShortcutKey(
"CtrlOrCmd+Shift+Down"
)}`,
"aria-label": t("labels.alignBottom"),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
var actionAlignLeft = register({
name: "alignLeft",
label: "labels.alignLeft",
icon: AlignLeftIcon,
trackEvent: { category: "element" },
predicate: (elements, appState, appProps, app) => alignActionsPredicate(appState, app),
perform: (elements, appState, _, app) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
position: "start",
axis: "x"
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
PanelComponent: ({ elements, appState, updateData, app }) => /* @__PURE__ */ jsx46(
ToolButton,
{
hidden: !alignActionsPredicate(appState, app),
type: "button",
icon: AlignLeftIcon,
onClick: () => updateData(null),
title: `${t("labels.alignLeft")} \u2014 ${getShortcutKey(
"CtrlOrCmd+Shift+Left"
)}`,
"aria-label": t("labels.alignLeft"),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
var actionAlignRight = register({
name: "alignRight",
label: "labels.alignRight",
icon: AlignRightIcon,
trackEvent: { category: "element" },
predicate: (elements, appState, appProps, app) => alignActionsPredicate(appState, app),
perform: (elements, appState, _, app) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
position: "end",
axis: "x"
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
PanelComponent: ({ elements, appState, updateData, app }) => /* @__PURE__ */ jsx46(
ToolButton,
{
hidden: !alignActionsPredicate(appState, app),
type: "button",
icon: AlignRightIcon,
onClick: () => updateData(null),
title: `${t("labels.alignRight")} \u2014 ${getShortcutKey(
"CtrlOrCmd+Shift+Right"
)}`,
"aria-label": t("labels.alignRight"),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
var actionAlignVerticallyCentered = register({
name: "alignVerticallyCentered",
label: "labels.centerVertically",
icon: CenterVerticallyIcon,
trackEvent: { category: "element" },
predicate: (elements, appState, appProps, app) => alignActionsPredicate(appState, app),
perform: (elements, appState, _, app) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
position: "center",
axis: "y"
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData, app }) => /* @__PURE__ */ jsx46(
ToolButton,
{
hidden: !alignActionsPredicate(appState, app),
type: "button",
icon: CenterVerticallyIcon,
onClick: () => updateData(null),
title: t("labels.centerVertically"),
"aria-label": t("labels.centerVertically"),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
var actionAlignHorizontallyCentered = register({
name: "alignHorizontallyCentered",
label: "labels.centerHorizontally",
icon: CenterHorizontallyIcon,
trackEvent: { category: "element" },
predicate: (elements, appState, appProps, app) => alignActionsPredicate(appState, app),
perform: (elements, appState, _, app) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
position: "center",
axis: "x"
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ elements, appState, updateData, app }) => /* @__PURE__ */ jsx46(
ToolButton,
{
hidden: !alignActionsPredicate(appState, app),
type: "button",
icon: CenterHorizontallyIcon,
onClick: () => updateData(null),
title: t("labels.centerHorizontally"),
"aria-label": t("labels.centerHorizontally"),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
// distribute.ts
var distributeElements = (selectedElements, elementsMap, distribution) => {
const [start, mid, end, extent] = distribution.axis === "x" ? ["minX", "midX", "maxX", "width"] : ["minY", "midY", "maxY", "height"];
const bounds = getCommonBoundingBox(selectedElements);
const groups = getMaximumGroups(selectedElements, elementsMap).map((group) => [group, getCommonBoundingBox(group)]).sort((a, b) => a[1][mid] - b[1][mid]);
let span = 0;
for (const group of groups) {
span += group[1][extent];
}
const step = (bounds[extent] - span) / (groups.length - 1);
if (step < 0) {
const index0 = groups.findIndex((g) => g[1][start] === bounds[start]);
const index1 = groups.findIndex((g) => g[1][end] === bounds[end]);
const step2 = (groups[index1][1][mid] - groups[index0][1][mid]) / (groups.length - 1);
let pos2 = groups[index0][1][mid];
return groups.flatMap(([group, box], index) => {
const translation = {
x: 0,
y: 0
};
if (index !== index0 && index !== index1) {
pos2 += step2;
translation[distribution.axis] = pos2 - box[mid];
}
return group.map(
(element) => newElementWith(element, {
x: element.x + translation.x,
y: element.y + translation.y
})
);
});
}
let pos = bounds[start];
return groups.flatMap(([group, box]) => {
const translation = {
x: 0,
y: 0
};
translation[distribution.axis] = pos - box[start];
pos += step;
pos += box[extent];
return group.map(
(element) => newElementWith(element, {
x: element.x + translation.x,
y: element.y + translation.y
})
);
});
};
// actions/actionDistribute.tsx
import { jsx as jsx47 } from "react/jsx-runtime";
var enableActionGroup2 = (appState, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
return selectedElements.length > 1 && // TODO enable distributing frames when implemented properly
!selectedElements.some((el) => isFrameLikeElement(el));
};
var distributeSelectedElements = (elements, appState, app, distribution) => {
const selectedElements = app.scene.getSelectedElements(appState);
const updatedElements = distributeElements(
selectedElements,
app.scene.getNonDeletedElementsMap(),
distribution
);
const updatedElementsMap = arrayToMap(updatedElements);
return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element),
appState,
app
);
};
var distributeHorizontally = register({
name: "distributeHorizontally",
label: "labels.distributeHorizontally",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
return {
appState,
elements: distributeSelectedElements(elements, appState, app, {
space: "between",
axis: "x"
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
PanelComponent: ({ elements, appState, updateData, app }) => /* @__PURE__ */ jsx47(
ToolButton,
{
hidden: !enableActionGroup2(appState, app),
type: "button",
icon: DistributeHorizontallyIcon,
onClick: () => updateData(null),
title: `${t("labels.distributeHorizontally")} \u2014 ${getShortcutKey(
"Alt+H"
)}`,
"aria-label": t("labels.distributeHorizontally"),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
var distributeVertically = register({
name: "distributeVertically",
label: "labels.distributeVertically",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
return {
appState,
elements: distributeSelectedElements(elements, appState, app, {
space: "between",
axis: "y"
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
PanelComponent: ({ elements, appState, updateData, app }) => /* @__PURE__ */ jsx47(
ToolButton,
{
hidden: !enableActionGroup2(appState, app),
type: "button",
icon: DistributeVerticallyIcon,
onClick: () => updateData(null),
title: `${t("labels.distributeVertically")} \u2014 ${getShortcutKey("Alt+V")}`,
"aria-label": t("labels.distributeVertically"),
visible: isSomeElementSelected(getNonDeletedElements(elements), appState)
}
)
});
// actions/actionFlip.ts
var actionFlipHorizontal = register({
name: "flipHorizontal",
label: "labels.flipHorizontal",
icon: flipHorizontal,
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
return {
elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(
elements,
app.scene.getNonDeletedElementsMap(),
appState,
"horizontal",
app
),
appState,
app
),
appState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event.shiftKey && event.code === CODES.H
});
var actionFlipVertical = register({
name: "flipVertical",
label: "labels.flipVertical",
icon: flipVertical,
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
return {
elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(
elements,
app.scene.getNonDeletedElementsMap(),
appState,
"vertical",
app
),
appState,
app
),
appState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event) => event.shiftKey && event.code === CODES.V && !event[KEYS.CTRL_OR_CMD]
});
var flipSelectedElements = (elements, elementsMap, appState, flipDirection, app) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true
}
);
const updatedElements = flipElements(
selectedElements,
elementsMap,
appState,
flipDirection,
app
);
const updatedElementsMap = arrayToMap(updatedElements);
return elements.map(
(element) => updatedElementsMap.get(element.id) || element
);
};
var flipElements = (selectedElements, elementsMap, appState, flipDirection, app) => {
if (selectedElements.every(
(element) => isArrowElement(element) && (element.startBinding || element.endBinding)
)) {
return selectedElements.map((element) => {
const _element = element;
return newElementWith(_element, {
startArrowhead: _element.endArrowhead,
endArrowhead: _element.startArrowhead
});
});
}
const { midX, midY } = getCommonBoundingBox(selectedElements);
resizeMultipleElements(
selectedElements,
elementsMap,
"nw",
app.scene,
new Map(
Array.from(elementsMap.values()).map((element) => [
element.id,
deepCopyElement(element)
])
),
{
flipByX: flipDirection === "horizontal",
flipByY: flipDirection === "vertical",
shouldResizeFromCenter: true,
shouldMaintainAspectRatio: true
}
);
bindOrUnbindLinearElements(
selectedElements.filter(isLinearElement),
elementsMap,
app.scene.getNonDeletedElements(),
app.scene,
isBindingEnabled(appState),
[],
appState.zoom
);
const { elbowArrows, otherElements } = selectedElements.reduce(
(acc, element) => isElbowArrow(element) ? { ...acc, elbowArrows: acc.elbowArrows.concat(element) } : { ...acc, otherElements: acc.otherElements.concat(element) },
{ elbowArrows: [], otherElements: [] }
);
const { midX: newMidX, midY: newMidY } = getCommonBoundingBox(selectedElements);
const [diffX, diffY] = [midX - newMidX, midY - newMidY];
otherElements.forEach(
(element) => mutateElement(element, {
x: element.x + diffX,
y: element.y + diffY
})
);
elbowArrows.forEach(
(element) => mutateElement(element, {
x: element.x + diffX,
y: element.y + diffY
})
);
return selectedElements;
};
// actions/actionClipboard.tsx
var actionCopy = register({
name: "copy",
label: "labels.copy",
icon: DuplicateIcon,
trackEvent: { category: "element" },
perform: async (elements, appState, event, app) => {
const elementsToCopy = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true
});
try {
await copyToClipboard(elementsToCopy, app.files, event);
} catch (error) {
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
appState: {
...appState,
errorMessage: error.message
}
};
}
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
// don't supply a shortcut since we handle this conditionally via onCopy event
keyTest: void 0
});
var actionPaste = register({
name: "paste",
label: "labels.paste",
trackEvent: { category: "element" },
perform: async (elements, appState, data, app) => {
let types;
try {
types = await readSystemClipboard();
} catch (error) {
if (error.name === "AbortError" || error.name === "NotAllowedError") {
return false;
}
console.error(`actionPaste ${error.name}: ${error.message}`);
if (isFirefox) {
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
appState: {
...appState,
errorMessage: t("hints.firefox_clipboard_write")
}
};
}
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
appState: {
...appState,
errorMessage: t("errors.asyncPasteFailedOnRead")
}
};
}
try {
app.pasteFromClipboard(createPasteEvent({ types }));
} catch (error) {
console.error(error);
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY,
appState: {
...appState,
errorMessage: t("errors.asyncPasteFailedOnParse")
}
};
}
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
// don't supply a shortcut since we handle this conditionally via onCopy event
keyTest: void 0
});
var actionCut = register({
name: "cut",
label: "labels.cut",
icon: cutIcon,
trackEvent: { category: "element" },
perform: (elements, appState, event, app) => {
actionCopy.perform(elements, appState, event, app);
return actionDeleteSelected.perform(elements, appState, null, app);
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X
});
var actionCopyAsSvg = register({
name: "copyAsSvg",
label: "labels.copyAsSvg",
icon: svgIcon,
trackEvent: { category: "element" },
perform: async (elements, appState, _data, app) => {
if (!app.canvas) {
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
const { exportedElements, exportingFrame } = prepareElementsForExport(
elements,
appState,
true
);
try {
await exportCanvas(
"clipboard-svg",
exportedElements,
appState,
app.files,
{
...appState,
exportingFrame,
name: app.getName()
}
);
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true
});
return {
appState: {
toast: {
message: t("toast.copyToClipboardAsSvg", {
exportSelection: selectedElements.length ? t("toast.selection") : t("toast.canvas"),
exportColorScheme: appState.exportWithDarkMode ? t("buttons.darkMode") : t("buttons.lightMode")
})
}
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
} catch (error) {
console.error(error);
return {
appState: {
errorMessage: error.message
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
},
predicate: (elements) => {
return probablySupportsClipboardWriteText && elements.length > 0;
},
keywords: ["svg", "clipboard", "copy"]
});
var actionCopyAsPng = register({
name: "copyAsPng",
label: "labels.copyAsPng",
icon: pngIcon,
trackEvent: { category: "element" },
perform: async (elements, appState, _data, app) => {
if (!app.canvas) {
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true
});
const { exportedElements, exportingFrame } = prepareElementsForExport(
elements,
appState,
true
);
try {
await exportCanvas("clipboard", exportedElements, appState, app.files, {
...appState,
exportingFrame,
name: app.getName()
});
return {
appState: {
...appState,
toast: {
message: t("toast.copyToClipboardAsPng", {
exportSelection: selectedElements.length ? t("toast.selection") : t("toast.canvas"),
exportColorScheme: appState.exportWithDarkMode ? t("buttons.darkMode") : t("buttons.lightMode")
})
}
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
} catch (error) {
console.error(error);
return {
appState: {
...appState,
errorMessage: error.message
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
},
predicate: (elements) => {
return probablySupportsClipboardBlob && elements.length > 0;
},
keyTest: (event) => event.code === CODES.C && event.altKey && event.shiftKey,
keywords: ["png", "clipboard", "copy"]
});
var copyText = register({
name: "copyText",
label: "labels.copyText",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true
});
try {
copyTextToSystemClipboard(getTextFromElements(selectedElements));
} catch (e) {
throw new Error(t("errors.copyToSystemClipboardFailed"));
}
return {
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
predicate: (elements, appState, _, app) => {
return probablySupportsClipboardWriteText && app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true
}).some(isTextElement);
},
keywords: ["text", "clipboard", "copy"]
});
// actions/actionToggleGridMode.tsx
var actionToggleGridMode = register({
name: "gridMode",
icon: gridIcon,
keywords: ["snap"],
label: "labels.toggleGrid",
viewMode: true,
trackEvent: {
category: "canvas",
predicate: (appState) => appState.gridModeEnabled
},
perform(elements, appState) {
return {
appState: {
...appState,
gridModeEnabled: !this.checked(appState),
objectsSnapModeEnabled: false
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
checked: (appState) => appState.gridModeEnabled,
predicate: (element, appState, props) => {
return props.gridModeEnabled === void 0;
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.code === CODES.QUOTE
});
// actions/actionToggleZenMode.tsx
var actionToggleZenMode = register({
name: "zenMode",
label: "buttons.zenMode",
icon: coffeeIcon,
paletteName: "Toggle zen mode",
viewMode: true,
trackEvent: {
category: "canvas",
predicate: (appState) => !appState.zenModeEnabled
},
perform(elements, appState) {
return {
appState: {
...appState,
zenModeEnabled: !this.checked(appState)
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
checked: (appState) => appState.zenModeEnabled,
predicate: (elements, appState, appProps) => {
return typeof appProps.zenModeEnabled === "undefined";
},
keyTest: (event) => !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.Z
});
// actions/actionToggleObjectsSnapMode.tsx
var actionToggleObjectsSnapMode = register({
name: "objectsSnapMode",
label: "buttons.objectsSnapMode",
icon: magnetIcon,
viewMode: false,
trackEvent: {
category: "canvas",
predicate: (appState) => !appState.objectsSnapModeEnabled
},
perform(elements, appState) {
return {
appState: {
...appState,
objectsSnapModeEnabled: !this.checked(appState),
gridModeEnabled: false
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
checked: (appState) => appState.objectsSnapModeEnabled,
predicate: (elements, appState, appProps) => {
return typeof appProps.objectsSnapModeEnabled === "undefined";
},
keyTest: (event) => !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.S
});
// actions/actionToggleStats.tsx
var actionToggleStats = register({
name: "stats",
label: "stats.fullTitle",
icon: abacusIcon,
paletteName: "Toggle stats",
viewMode: true,
trackEvent: { category: "menu" },
keywords: ["edit", "attributes", "customize"],
perform(elements, appState) {
return {
appState: {
...appState,
stats: { ...appState.stats, open: !this.checked(appState) }
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
checked: (appState) => appState.stats.open,
keyTest: (event) => !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.SLASH
});
// actions/actionBoundText.tsx
var actionUnbindText = register({
name: "unbindText",
label: "labels.unbindText",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
return selectedElements.some((element) => hasBoundTextElement(element));
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
const elementsMap = app.scene.getNonDeletedElementsMap();
selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement) {
const { width, height } = measureText(
boundTextElement.originalText,
getFontString(boundTextElement),
boundTextElement.lineHeight
);
const originalContainerHeight = getOriginalContainerHeightFromCache(
element.id
);
resetOriginalContainerCache(element.id);
const { x, y } = computeBoundTextPosition(
element,
boundTextElement,
elementsMap
);
mutateElement(boundTextElement, {
containerId: null,
width,
height,
text: boundTextElement.originalText,
x,
y
});
mutateElement(element, {
boundElements: element.boundElements?.filter(
(ele) => ele.id !== boundTextElement.id
),
height: originalContainerHeight ? originalContainerHeight : element.height
});
}
});
return {
elements,
appState,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
});
var actionBindText = register({
name: "bindText",
label: "labels.bindText",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
if (selectedElements.length === 2) {
const textElement = isTextElement(selectedElements[0]) || isTextElement(selectedElements[1]);
let bindingContainer;
if (isTextBindableContainer(selectedElements[0])) {
bindingContainer = selectedElements[0];
} else if (isTextBindableContainer(selectedElements[1])) {
bindingContainer = selectedElements[1];
}
if (textElement && bindingContainer && getBoundTextElement(
bindingContainer,
app.scene.getNonDeletedElementsMap()
) === null) {
return true;
}
}
return false;
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
let textElement;
let container;
if (isTextElement(selectedElements[0]) && isTextBindableContainer(selectedElements[1])) {
textElement = selectedElements[0];
container = selectedElements[1];
} else {
textElement = selectedElements[1];
container = selectedElements[0];
}
mutateElement(textElement, {
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
textAlign: TEXT_ALIGN.CENTER,
autoResize: true
});
mutateElement(container, {
boundElements: (container.boundElements || []).concat({
type: "text",
id: textElement.id
})
});
const originalContainerHeight = container.height;
redrawTextBoundingBox(
textElement,
container,
app.scene.getNonDeletedElementsMap()
);
updateOriginalContainerCache(container.id, originalContainerHeight);
return {
elements: pushTextAboveContainer(elements, container, textElement),
appState: { ...appState, selectedElementIds: { [container.id]: true } },
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
});
var pushTextAboveContainer = (elements, container, textElement) => {
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);
syncMovedIndices(updatedElements, arrayToMap([container, textElement]));
return updatedElements;
};
var pushContainerBelowText = (elements, container, textElement) => {
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);
syncMovedIndices(updatedElements, arrayToMap([container, textElement]));
return updatedElements;
};
var actionWrapTextInContainer = register({
name: "wrapTextInContainer",
label: "labels.createContainerFromText",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
const areTextElements = selectedElements.every((el) => isTextElement(el));
return selectedElements.length > 0 && areTextElements;
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
let updatedElements = elements.slice();
const containerIds = {};
for (const textElement of selectedElements) {
if (isTextElement(textElement)) {
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,
frameId: textElement.frameId
});
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)
);
linearElements.forEach((ele) => {
let startBinding = ele.startBinding;
let endBinding = ele.endBinding;
if (startBinding?.elementId === textElement.id) {
startBinding = {
...startBinding,
elementId: container.id
};
}
if (endBinding?.elementId === textElement.id) {
endBinding = { ...endBinding, elementId: container.id };
}
if (startBinding || endBinding) {
mutateElement(ele, { startBinding, endBinding }, false);
}
});
}
mutateElement(
textElement,
{
containerId: container.id,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
boundElements: null,
textAlign: TEXT_ALIGN.CENTER,
autoResize: true
},
false
);
redrawTextBoundingBox(
textElement,
container,
app.scene.getNonDeletedElementsMap()
);
updatedElements = pushContainerBelowText(
[...updatedElements, container],
container,
textElement
);
containerIds[container.id] = true;
}
}
return {
elements: updatedElements,
appState: {
...appState,
selectedElementIds: containerIds
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
});
// components/hyperlink/Hyperlink.tsx
import {
useCallback as useCallback4,
useEffect as useEffect17,
useLayoutEffect as useLayoutEffect2,
useRef as useRef12,
useState as useState11
} from "react";
import clsx19 from "clsx";
import { jsx as jsx48, jsxs as jsxs26 } from "react/jsx-runtime";
var POPUP_WIDTH = 380;
var POPUP_HEIGHT = 42;
var POPUP_PADDING = 5;
var SPACE_BOTTOM = 85;
var AUTO_HIDE_TIMEOUT = 500;
var IS_HYPERLINK_TOOLTIP_VISIBLE = false;
var embeddableLinkCache = /* @__PURE__ */ new Map();
var Hyperlink = ({
element,
elementsMap,
setAppState,
onLinkOpen,
setToast,
updateEmbedValidationStatus
}) => {
const appState = useExcalidrawAppState();
const appProps = useAppProps();
const device = useDevice();
const linkVal = element.link || "";
const [inputVal, setInputVal] = useState11(linkVal);
const inputRef = useRef12(null);
const isEditing = appState.showHyperlinkPopup === "editor";
const handleSubmit = useCallback4(() => {
if (!inputRef.current) {
return;
}
const link = normalizeLink(inputRef.current.value) || null;
if (!element.link && link) {
trackEvent("hyperlink", "create");
}
if (isEmbeddableElement(element)) {
if (appState.activeEmbeddable?.element === element) {
setAppState({ activeEmbeddable: null });
}
if (!link) {
mutateElement(element, {
link: null
});
updateEmbedValidationStatus(element, false);
return;
}
if (!embeddableURLValidator(link, appProps.validateEmbeddable)) {
if (link) {
setToast({ message: t("toast.unableToEmbed"), closable: true });
}
element.link && embeddableLinkCache.set(element.id, element.link);
mutateElement(element, {
link
});
updateEmbedValidationStatus(element, false);
} else {
const { width, height } = element;
const embedLink = getEmbedLink(link);
if (embedLink?.error instanceof URIError) {
setToast({
message: t("toast.unrecognizedLinkFormat"),
closable: true
});
}
const ar = embedLink ? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h : 1;
const hasLinkChanged = embeddableLinkCache.get(element.id) !== element.link;
mutateElement(element, {
...hasLinkChanged ? {
width: embedLink?.type === "video" ? width > height ? width : height * ar : width,
height: embedLink?.type === "video" ? width > height ? width / ar : height : height
} : {},
link
});
updateEmbedValidationStatus(element, true);
if (embeddableLinkCache.has(element.id)) {
embeddableLinkCache.delete(element.id);
}
}
} else {
mutateElement(element, { link });
}
}, [
element,
setToast,
appProps.validateEmbeddable,
appState.activeEmbeddable,
setAppState,
updateEmbedValidationStatus
]);
useLayoutEffect2(() => {
return () => {
handleSubmit();
};
}, [handleSubmit]);
useEffect17(() => {
if (isEditing && inputRef?.current && !(device.viewport.isMobile || device.isTouchScreen)) {
inputRef.current.select();
}
}, [isEditing, device.viewport.isMobile, device.isTouchScreen]);
useEffect17(() => {
let timeoutId = null;
const handlePointerMove = (event) => {
if (isEditing) {
return;
}
if (timeoutId) {
clearTimeout(timeoutId);
}
const shouldHide = shouldHideLinkPopup(
element,
elementsMap,
appState,
pointFrom(event.clientX, event.clientY)
);
if (shouldHide) {
timeoutId = window.setTimeout(() => {
setAppState({ showHyperlinkPopup: false });
}, AUTO_HIDE_TIMEOUT);
}
};
window.addEventListener("pointermove" /* POINTER_MOVE */, handlePointerMove, false);
return () => {
window.removeEventListener("pointermove" /* POINTER_MOVE */, handlePointerMove, false);
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [appState, element, isEditing, setAppState, elementsMap]);
const handleRemove = useCallback4(() => {
trackEvent("hyperlink", "delete");
mutateElement(element, { link: null });
setAppState({ showHyperlinkPopup: false });
}, [setAppState, element]);
const onEdit = () => {
trackEvent("hyperlink", "edit", "popup-ui");
setAppState({ showHyperlinkPopup: "editor" });
};
const { x, y } = getCoordsForPopover(element, appState, elementsMap);
if (appState.contextMenu || appState.selectedElementsAreBeingDragged || appState.resizingElement || appState.isRotating || appState.openMenu || appState.viewModeEnabled) {
return null;
}
return /* @__PURE__ */ jsxs26(
"div",
{
className: "excalidraw-hyperlinkContainer",
style: {
top: `${y}px`,
left: `${x}px`,
width: POPUP_WIDTH,
padding: POPUP_PADDING
},
children: [
isEditing ? /* @__PURE__ */ jsx48(
"input",
{
className: clsx19("excalidraw-hyperlinkContainer-input"),
placeholder: t("labels.link.hint"),
ref: inputRef,
value: inputVal,
onChange: (event) => setInputVal(event.target.value),
autoFocus: true,
onKeyDown: (event) => {
event.stopPropagation();
if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K) {
event.preventDefault();
}
if (event.key === KEYS.ENTER || event.key === KEYS.ESCAPE) {
handleSubmit();
setAppState({ showHyperlinkPopup: "info" });
}
}
}
) : element.link ? /* @__PURE__ */ jsx48(
"a",
{
href: normalizeLink(element.link || ""),
className: "excalidraw-hyperlinkContainer-link",
target: isLocalLink(element.link) ? "_self" : "_blank",
onClick: (event) => {
if (element.link && onLinkOpen) {
const customEvent = wrapEvent(
"excalidraw-link" /* EXCALIDRAW_LINK */,
event.nativeEvent
);
onLinkOpen(
{
...element,
link: normalizeLink(element.link)
},
customEvent
);
if (customEvent.defaultPrevented) {
event.preventDefault();
}
}
},
rel: "noopener noreferrer",
children: element.link
}
) : /* @__PURE__ */ jsx48("div", { className: "excalidraw-hyperlinkContainer-link", children: t("labels.link.empty") }),
/* @__PURE__ */ jsxs26("div", { className: "excalidraw-hyperlinkContainer__buttons", children: [
!isEditing && /* @__PURE__ */ jsx48(
ToolButton,
{
type: "button",
title: t("buttons.edit"),
"aria-label": t("buttons.edit"),
label: t("buttons.edit"),
onClick: onEdit,
className: "excalidraw-hyperlinkContainer--edit",
icon: FreedrawIcon
}
),
/* @__PURE__ */ jsx48(
ToolButton,
{
type: "button",
title: t("labels.linkToElement"),
"aria-label": t("labels.linkToElement"),
label: t("labels.linkToElement"),
onClick: () => {
setAppState({
openDialog: {
name: "elementLinkSelector",
sourceElementId: element.id
}
});
},
icon: elementLinkIcon
}
),
linkVal && !isEmbeddableElement(element) && /* @__PURE__ */ jsx48(
ToolButton,
{
type: "button",
title: t("buttons.remove"),
"aria-label": t("buttons.remove"),
label: t("buttons.remove"),
onClick: handleRemove,
className: "excalidraw-hyperlinkContainer--remove",
icon: TrashIcon
}
)
] })
]
}
);
};
var getCoordsForPopover = (element, appState, elementsMap) => {
const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width / 2, sceneY: y1 },
appState
);
const x = viewportX - appState.offsetLeft - POPUP_WIDTH / 2;
const y = viewportY - appState.offsetTop - SPACE_BOTTOM;
return { x, y };
};
var getContextMenuLabel = (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
const label = isEmbeddableElement(selectedElements[0]) ? "labels.link.editEmbed" : selectedElements[0]?.link ? "labels.link.edit" : "labels.link.create";
return label;
};
var HYPERLINK_TOOLTIP_TIMEOUT_ID = null;
var showHyperlinkTooltip = (element, appState, elementsMap) => {
if (HYPERLINK_TOOLTIP_TIMEOUT_ID) {
clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID);
}
HYPERLINK_TOOLTIP_TIMEOUT_ID = window.setTimeout(
() => renderTooltip(element, appState, elementsMap),
HYPERLINK_TOOLTIP_DELAY
);
};
var renderTooltip = (element, appState, elementsMap) => {
if (!element.link) {
return;
}
const tooltipDiv = getTooltipDiv();
tooltipDiv.classList.add("excalidraw-tooltip--visible");
tooltipDiv.style.maxWidth = "20rem";
tooltipDiv.textContent = isElementLink(element.link) ? t("labels.link.goToElement") : element.link;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
[x1, y1, x2, y2],
element.angle,
appState
);
const linkViewportCoords = sceneCoordsToViewportCoords(
{ sceneX: linkX, sceneY: linkY },
appState
);
updateTooltipPosition(
tooltipDiv,
{
left: linkViewportCoords.x,
top: linkViewportCoords.y,
width: linkWidth,
height: linkHeight
},
"top"
);
trackEvent("hyperlink", "tooltip", "link-icon");
IS_HYPERLINK_TOOLTIP_VISIBLE = true;
};
var hideHyperlinkToolip = () => {
if (HYPERLINK_TOOLTIP_TIMEOUT_ID) {
clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID);
}
if (IS_HYPERLINK_TOOLTIP_VISIBLE) {
IS_HYPERLINK_TOOLTIP_VISIBLE = false;
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
}
};
var shouldHideLinkPopup = (element, elementsMap, appState, [clientX, clientY]) => {
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
{ clientX, clientY },
appState
);
const threshold = 15 / appState.zoom.value;
if (hitElementBoundingBox(sceneX, sceneY, element, elementsMap)) {
return false;
}
const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
if (sceneX >= x1 && sceneX <= x2 && sceneY >= y1 - SPACE_BOTTOM && sceneY <= y1) {
return false;
}
const { x: popoverX, y: popoverY } = getCoordsForPopover(
element,
appState,
elementsMap
);
if (clientX >= popoverX - threshold && clientX <= popoverX + POPUP_WIDTH + POPUP_PADDING * 2 + threshold && clientY >= popoverY - threshold && clientY <= popoverY + threshold + POPUP_PADDING * 2 + POPUP_HEIGHT) {
return false;
}
return true;
};
// actions/actionLink.tsx
import { jsx as jsx49 } from "react/jsx-runtime";
var actionLink = register({
name: "hyperlink",
label: (elements, appState) => getContextMenuLabel(elements, appState),
icon: LinkIcon,
perform: (elements, appState) => {
if (appState.showHyperlinkPopup === "editor") {
return false;
}
return {
elements,
appState: {
...appState,
showHyperlinkPopup: "editor",
openMenu: null
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
trackEvent: { category: "hyperlink", action: "click" },
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length === 1;
},
PanelComponent: ({ elements, appState, updateData }) => {
const selectedElements = getSelectedElements(elements, appState);
return /* @__PURE__ */ jsx49(
ToolButton,
{
type: "button",
icon: LinkIcon,
"aria-label": t(getContextMenuLabel(elements, appState)),
title: `${isEmbeddableElement(elements[0]) ? t("labels.link.labelEmbed") : t("labels.link.label")} - ${getShortcutKey("CtrlOrCmd+K")}`,
onClick: () => updateData(null),
selected: selectedElements.length === 1 && !!selectedElements[0].link
}
);
}
});
// actions/actionElementLock.ts
var shouldLock = (elements) => elements.every((el) => !el.locked);
var actionToggleElementLock = register({
name: "toggleElementLock",
label: (elements, appState, app) => {
const selected = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: false
});
if (selected.length === 1 && !isFrameLikeElement(selected[0])) {
return selected[0].locked ? "labels.elementLock.unlock" : "labels.elementLock.lock";
}
return shouldLock(selected) ? "labels.elementLock.lockAll" : "labels.elementLock.unlockAll";
},
icon: (appState, elements) => {
const selectedElements = getSelectedElements(elements, appState);
return shouldLock(selectedElements) ? LockedIcon : UnlockedIcon;
},
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
return selectedElements.length > 0 && !selectedElements.some((element) => element.locked && element.frameId);
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true
});
if (!selectedElements.length) {
return false;
}
const nextLockState = shouldLock(selectedElements);
const selectedElementsMap = arrayToMap(selectedElements);
return {
elements: elements.map((element) => {
if (!selectedElementsMap.has(element.id)) {
return element;
}
return newElementWith(element, { locked: nextLockState });
}),
appState: {
...appState,
selectedLinearElement: nextLockState ? null : appState.selectedLinearElement
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
keyTest: (event, appState, elements, app) => {
return event.key.toLocaleLowerCase() === KEYS.L && event[KEYS.CTRL_OR_CMD] && event.shiftKey && app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: false
}).length > 0;
}
});
var actionUnlockAllElements = register({
name: "unlockAllElements",
paletteName: "Unlock all elements",
trackEvent: { category: "canvas" },
viewMode: false,
icon: UnlockedIcon,
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length === 0 && elements.some((element) => element.locked);
},
perform: (elements, appState) => {
const lockedElements = elements.filter((el) => el.locked);
return {
elements: elements.map((element) => {
if (element.locked) {
return newElementWith(element, { locked: false });
}
return element;
}),
appState: {
...appState,
selectedElementIds: Object.fromEntries(
lockedElements.map((el) => [el.id, true])
)
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
label: "labels.elementLock.unlockAll"
});
// components/CommandPalette/CommandPalette.tsx
import { useEffect as useEffect28, useRef as useRef22, useState as useState23 } from "react";
// components/Dialog.tsx
import clsx28 from "clsx";
import { useEffect as useEffect27, useState as useState20 } from "react";
// hooks/useCallbackRefState.ts
import { useCallback as useCallback5, useState as useState12 } from "react";
var useCallbackRefState = () => {
const [refValue, setRefValue] = useState12(null);
const refCallback = useCallback5((value) => setRefValue(value), []);
return [refValue, refCallback];
};
// components/Modal.tsx
import { createPortal as createPortal2 } from "react-dom";
import clsx20 from "clsx";
import { useRef as useRef13 } from "react";
import { jsx as jsx50, jsxs as jsxs27 } from "react/jsx-runtime";
var Modal = (props) => {
const { closeOnClickOutside = true } = props;
const modalRoot = useCreatePortalContainer({
className: "excalidraw-modal-container"
});
const animationsDisabledRef = useRef13(
document.body.classList.contains("excalidraw-animations-disabled")
);
if (!modalRoot) {
return null;
}
const handleKeydown = (event) => {
if (event.key === KEYS.ESCAPE) {
event.nativeEvent.stopImmediatePropagation();
event.stopPropagation();
props.onCloseRequest();
}
};
return createPortal2(
/* @__PURE__ */ jsxs27(
"div",
{
className: clsx20("Modal", props.className, {
"animations-disabled": animationsDisabledRef.current
}),
role: "dialog",
"aria-modal": "true",
onKeyDown: handleKeydown,
"aria-labelledby": props.labelledBy,
"data-prevent-outside-click": true,
children: [
/* @__PURE__ */ jsx50(
"div",
{
className: "Modal__background",
onClick: closeOnClickOutside ? props.onCloseRequest : void 0
}
),
/* @__PURE__ */ jsx50(
"div",
{
className: "Modal__content",
style: { "--max-width": `${props.maxWidth}px` },
tabIndex: 0,
children: props.children
}
)
]
}
),
modalRoot
);
};
// components/LibraryMenu.tsx
import {
useState as useState19,
useCallback as useCallback10,
useMemo as useMemo6,
useEffect as useEffect26,
memo as memo3,
useRef as useRef19
} from "react";
// data/library.ts
import { useEffect as useEffect19, useRef as useRef14 } from "react";
// hooks/useLibraryItemSvg.ts
import { useEffect as useEffect18, useState as useState13 } from "react";
var libraryItemSvgsCache = atom(/* @__PURE__ */ new Map());
var exportLibraryItemToSvg = async (elements) => {
return await exportToSvg2({
elements,
appState: {
exportBackground: false,
viewBackgroundColor: COLOR_PALETTE.white
},
files: null,
renderEmbeddables: false,
skipInliningFonts: true
});
};
var useLibraryItemSvg = (id, elements, svgCache) => {
const [svg, setSvg] = useState13();
useEffect18(() => {
if (elements) {
if (id) {
const cachedSvg = svgCache.get(id);
if (cachedSvg) {
setSvg(cachedSvg);
} else {
(async () => {
const exportedSvg = await exportLibraryItemToSvg(elements);
exportedSvg.querySelector(".style-fonts")?.remove();
if (exportedSvg) {
svgCache.set(id, exportedSvg);
setSvg(exportedSvg);
}
})();
}
} else {
(async () => {
const exportedSvg = await exportLibraryItemToSvg(elements);
setSvg(exportedSvg);
})();
}
}
}, [id, elements, svgCache, setSvg]);
return svg;
};
var useLibraryCache = () => {
const [svgCache] = useAtom(libraryItemSvgsCache);
const clearLibraryCache = () => svgCache.clear();
const deleteItemsFromLibraryCache = (items) => {
items.forEach((item) => svgCache.delete(item));
};
return {
clearLibraryCache,
deleteItemsFromLibraryCache,
svgCache
};
};
// queue.ts
var Queue = class {
constructor() {
__publicField(this, "jobs", []);
__publicField(this, "running", false);
}
tick() {
if (this.running) {
return;
}
const job = this.jobs.shift();
if (job) {
this.running = true;
job.promise.resolve(
promiseTry(job.jobFactory, ...job.args).finally(() => {
this.running = false;
this.tick();
})
);
} else {
this.running = false;
}
}
push(jobFactory, ...args) {
const promise = resolvablePromise();
this.jobs.push({ jobFactory, promise, args });
this.tick();
return promise;
}
};
// data/library.ts
var ALLOWED_LIBRARY_URLS = [
"excalidraw.com",
// when installing from github PRs
"raw.githubusercontent.com/excalidraw/excalidraw-libraries"
];
var onLibraryUpdateEmitter = new Emitter();
var libraryItemsAtom = atom({ status: "loaded", isInitialized: false, libraryItems: [] });
var cloneLibraryItems = (libraryItems) => cloneJSON(libraryItems);
var isUniqueItem = (existingLibraryItems, targetLibraryItem) => {
return !existingLibraryItems.find((libraryItem) => {
if (libraryItem.elements.length !== targetLibraryItem.elements.length) {
return false;
}
return libraryItem.elements.every((libItemExcalidrawItem, idx) => {
return libItemExcalidrawItem.id === targetLibraryItem.elements[idx].id && libItemExcalidrawItem.versionNonce === targetLibraryItem.elements[idx].versionNonce;
});
});
};
var mergeLibraryItems = (localItems, otherItems) => {
const newItems = [];
for (const item of otherItems) {
if (isUniqueItem(localItems, item)) {
newItems.push(item);
}
}
return [...newItems, ...localItems];
};
var createLibraryUpdate = (prevLibraryItems, nextLibraryItems) => {
const nextItemsMap = arrayToMap(nextLibraryItems);
const update = {
deletedItems: /* @__PURE__ */ new Map(),
addedItems: /* @__PURE__ */ new Map()
};
for (const item of prevLibraryItems) {
if (!nextItemsMap.has(item.id)) {
update.deletedItems.set(item.id, item);
}
}
const prevItemsMap = arrayToMap(prevLibraryItems);
for (const item of nextLibraryItems) {
if (!prevItemsMap.has(item.id)) {
update.addedItems.set(item.id, item);
}
}
return update;
};
var Library = class {
constructor(app) {
/** latest libraryItems */
__publicField(this, "currLibraryItems", []);
/** snapshot of library items since last onLibraryChange call */
__publicField(this, "prevLibraryItems", cloneLibraryItems(this.currLibraryItems));
__publicField(this, "app");
__publicField(this, "updateQueue", []);
__publicField(this, "getLastUpdateTask", () => {
return this.updateQueue[this.updateQueue.length - 1];
});
__publicField(this, "notifyListeners", () => {
if (this.updateQueue.length > 0) {
editorJotaiStore.set(libraryItemsAtom, (s) => ({
status: "loading",
libraryItems: this.currLibraryItems,
isInitialized: s.isInitialized
}));
} else {
editorJotaiStore.set(libraryItemsAtom, {
status: "loaded",
libraryItems: this.currLibraryItems,
isInitialized: true
});
try {
const prevLibraryItems = this.prevLibraryItems;
this.prevLibraryItems = cloneLibraryItems(this.currLibraryItems);
const nextLibraryItems = cloneLibraryItems(this.currLibraryItems);
this.app.props.onLibraryChange?.(nextLibraryItems);
onLibraryUpdateEmitter.trigger(
createLibraryUpdate(prevLibraryItems, nextLibraryItems),
nextLibraryItems
);
} catch (error) {
console.error(error);
}
}
});
/** call on excalidraw instance unmount */
__publicField(this, "destroy", () => {
this.updateQueue = [];
this.currLibraryItems = [];
editorJotaiStore.set(libraryItemSvgsCache, /* @__PURE__ */ new Map());
});
__publicField(this, "resetLibrary", () => {
return this.setLibrary([]);
});
/**
* @returns latest cloned libraryItems. Awaits all in-progress updates first.
*/
__publicField(this, "getLatestLibrary", () => {
return new Promise(async (resolve) => {
try {
const libraryItems = await (this.getLastUpdateTask() || this.currLibraryItems);
if (this.updateQueue.length > 0) {
resolve(this.getLatestLibrary());
} else {
resolve(cloneLibraryItems(libraryItems));
}
} catch (error) {
return resolve(this.currLibraryItems);
}
});
});
// NOTE this is a high-level public API (exposed on ExcalidrawAPI) with
// a slight overhead (always restoring library items). For internal use
// where merging isn't needed, use `library.setLibrary()` directly.
__publicField(this, "updateLibrary", async ({
libraryItems,
prompt = false,
merge = false,
openLibraryMenu = false,
defaultStatus = "unpublished"
}) => {
if (openLibraryMenu) {
this.app.setState({
openSidebar: { name: DEFAULT_SIDEBAR.name, tab: LIBRARY_SIDEBAR_TAB }
});
}
return this.setLibrary(() => {
return new Promise(async (resolve, reject) => {
try {
const source = await (typeof libraryItems === "function" && !(libraryItems instanceof Blob) ? libraryItems(this.currLibraryItems) : libraryItems);
let nextItems;
if (source instanceof Blob) {
nextItems = await loadLibraryFromBlob(source, defaultStatus);
} else {
nextItems = restoreLibraryItems(source, defaultStatus);
}
if (!prompt || window.confirm(
t("alerts.confirmAddLibrary", {
numShapes: nextItems.length
})
)) {
if (prompt) {
this.app.focusContainer();
}
if (merge) {
resolve(mergeLibraryItems(this.currLibraryItems, nextItems));
} else {
resolve(nextItems);
}
} else {
reject(new AbortError());
}
} catch (error) {
reject(error);
}
});
});
});
__publicField(this, "setLibrary", (libraryItems) => {
const task = new Promise(async (resolve, reject) => {
try {
await this.getLastUpdateTask();
if (typeof libraryItems === "function") {
libraryItems = libraryItems(this.currLibraryItems);
}
this.currLibraryItems = cloneLibraryItems(await libraryItems);
resolve(this.currLibraryItems);
} catch (error) {
reject(error);
}
}).catch((error) => {
if (error.name === "AbortError") {
console.warn("Library update aborted by user");
return this.currLibraryItems;
}
throw error;
}).finally(() => {
this.updateQueue = this.updateQueue.filter((_task) => _task !== task);
this.notifyListeners();
});
this.updateQueue.push(task);
this.notifyListeners();
return task;
});
this.app = app;
}
};
var library_default = Library;
var distributeLibraryItemsOnSquareGrid = (libraryItems) => {
const PADDING = 50;
const ITEMS_PER_ROW = Math.ceil(Math.sqrt(libraryItems.length));
const resElements = [];
const getMaxHeightPerRow = (row2) => {
const maxHeight = libraryItems.slice(row2 * ITEMS_PER_ROW, row2 * ITEMS_PER_ROW + ITEMS_PER_ROW).reduce((acc, item) => {
const { height } = getCommonBoundingBox(item.elements);
return Math.max(acc, height);
}, 0);
return maxHeight;
};
const getMaxWidthPerCol = (targetCol) => {
let index2 = 0;
let currCol = 0;
let maxWidth = 0;
for (const item of libraryItems) {
if (index2 % ITEMS_PER_ROW === 0) {
currCol = 0;
}
if (currCol === targetCol) {
const { width } = getCommonBoundingBox(item.elements);
maxWidth = Math.max(maxWidth, width);
}
index2++;
currCol++;
}
return maxWidth;
};
let colOffsetX = 0;
let rowOffsetY = 0;
let maxHeightCurrRow = 0;
let maxWidthCurrCol = 0;
let index = 0;
let col = 0;
let row = 0;
for (const item of libraryItems) {
if (index && index % ITEMS_PER_ROW === 0) {
rowOffsetY += maxHeightCurrRow + PADDING;
colOffsetX = 0;
col = 0;
row++;
}
if (col === 0) {
maxHeightCurrRow = getMaxHeightPerRow(row);
}
maxWidthCurrCol = getMaxWidthPerCol(col);
const { minX, minY, width, height } = getCommonBoundingBox(item.elements);
const offsetCenterX = (maxWidthCurrCol - width) / 2;
const offsetCenterY = (maxHeightCurrRow - height) / 2;
resElements.push(
...item.elements.map((element) => ({
...element,
x: element.x + // offset for column
colOffsetX + // offset to center in given square grid
offsetCenterX - // subtract minX so that given item starts at 0 coord
minX,
y: element.y + // offset for row
rowOffsetY + // offset to center in given square grid
offsetCenterY - // subtract minY so that given item starts at 0 coord
minY
}))
);
colOffsetX += maxWidthCurrCol + PADDING;
index++;
col++;
}
return resElements;
};
var validateLibraryUrl = (libraryUrl, validator = ALLOWED_LIBRARY_URLS) => {
if (typeof validator === "function" ? validator(libraryUrl) : validator.some((allowedUrlDef) => {
const allowedUrl = new URL(
`https://${allowedUrlDef.replace(/^https?:\/\//, "")}`
);
const { hostname, pathname } = new URL(libraryUrl);
return new RegExp(`(^|\\.)${allowedUrl.hostname}$`).test(hostname) && new RegExp(
`^${allowedUrl.pathname.replace(/\/+$/, "")}(/+|$)`
).test(pathname);
})) {
return true;
}
throw new Error(`Invalid or disallowed library URL: "${libraryUrl}"`);
};
var parseLibraryTokensFromUrl = () => {
const libraryUrl = (
// current
new URLSearchParams(window.location.hash.slice(1)).get(
URL_HASH_KEYS.addLibrary
) || // legacy, kept for compat reasons
new URLSearchParams(window.location.search).get(URL_QUERY_KEYS.addLibrary)
);
const idToken = libraryUrl ? new URLSearchParams(window.location.hash.slice(1)).get("token") : null;
return libraryUrl ? { libraryUrl, idToken } : null;
};
var _AdapterTransaction = class _AdapterTransaction {
constructor(adapter) {
// ------------------
__publicField(this, "adapter");
this.adapter = adapter;
}
static async getLibraryItems(adapter, source, _queue = true) {
const task = () => new Promise(async (resolve, reject) => {
try {
const data = await adapter.load({ source });
resolve(restoreLibraryItems(data?.libraryItems || [], "published"));
} catch (error) {
reject(error);
}
});
if (_queue) {
return _AdapterTransaction.queue.push(task);
}
return task();
}
getLibraryItems(source) {
return _AdapterTransaction.getLibraryItems(this.adapter, source, false);
}
};
__publicField(_AdapterTransaction, "queue", new Queue());
__publicField(_AdapterTransaction, "run", async (adapter, fn) => {
const transaction = new _AdapterTransaction(adapter);
return _AdapterTransaction.queue.push(() => fn(transaction));
});
var AdapterTransaction = _AdapterTransaction;
var lastSavedLibraryItemsHash = 0;
var librarySaveCounter = 0;
var getLibraryItemsHash = (items) => {
return hashString(
items.map((item) => {
return `${item.id}:${hashElementsVersion(item.elements)}`;
}).sort().join()
);
};
var persistLibraryUpdate = async (adapter, update) => {
try {
librarySaveCounter++;
return await AdapterTransaction.run(adapter, async (transaction) => {
const nextLibraryItemsMap = arrayToMap(
await transaction.getLibraryItems("save")
);
for (const [id] of update.deletedItems) {
nextLibraryItemsMap.delete(id);
}
const addedItems = [];
for (const [id, item] of update.addedItems) {
if (nextLibraryItemsMap.has(id)) {
nextLibraryItemsMap.set(id, item);
} else {
addedItems.push(item);
}
}
const nextLibraryItems = addedItems.concat(
Array.from(nextLibraryItemsMap.values())
);
const version = getLibraryItemsHash(nextLibraryItems);
if (version !== lastSavedLibraryItemsHash) {
await adapter.save({ libraryItems: nextLibraryItems });
}
lastSavedLibraryItemsHash = version;
return nextLibraryItems;
});
} finally {
librarySaveCounter--;
}
};
var useHandleLibrary = (opts) => {
const { excalidrawAPI } = opts;
const optsRef = useRef14(opts);
optsRef.current = opts;
const isLibraryLoadedRef = useRef14(false);
useEffect19(() => {
if (!excalidrawAPI) {
return;
}
isLibraryLoadedRef.current = false;
const importLibraryFromURL = async ({
libraryUrl,
idToken
}) => {
const libraryPromise = new Promise(async (resolve, reject) => {
try {
libraryUrl = decodeURIComponent(libraryUrl);
libraryUrl = toValidURL(libraryUrl);
validateLibraryUrl(libraryUrl, optsRef.current.validateLibraryUrl);
const request = await fetch(libraryUrl);
const blob = await request.blob();
resolve(blob);
} catch (error) {
reject(error);
}
});
const shouldPrompt = idToken !== excalidrawAPI.id;
await (shouldPrompt && document.hidden ? new Promise((resolve) => {
window.addEventListener("focus", () => resolve(), {
once: true
});
}) : null);
try {
await excalidrawAPI.updateLibrary({
libraryItems: libraryPromise,
prompt: shouldPrompt,
merge: true,
defaultStatus: "published",
openLibraryMenu: true
});
} catch (error) {
excalidrawAPI.updateScene({
appState: {
errorMessage: error.message
}
});
throw error;
} finally {
if (window.location.hash.includes(URL_HASH_KEYS.addLibrary)) {
const hash = new URLSearchParams(window.location.hash.slice(1));
hash.delete(URL_HASH_KEYS.addLibrary);
window.history.replaceState({}, APP_NAME, `#${hash.toString()}`);
} else if (window.location.search.includes(URL_QUERY_KEYS.addLibrary)) {
const query = new URLSearchParams(window.location.search);
query.delete(URL_QUERY_KEYS.addLibrary);
window.history.replaceState({}, APP_NAME, `?${query.toString()}`);
}
}
};
const onHashChange = (event) => {
event.preventDefault();
const libraryUrlTokens2 = parseLibraryTokensFromUrl();
if (libraryUrlTokens2) {
event.stopImmediatePropagation();
window.history.replaceState({}, "", event.oldURL);
importLibraryFromURL(libraryUrlTokens2);
}
};
const libraryUrlTokens = parseLibraryTokensFromUrl();
if (libraryUrlTokens) {
importLibraryFromURL(libraryUrlTokens);
}
if ("getInitialLibraryItems" in optsRef.current && optsRef.current.getInitialLibraryItems) {
console.warn(
"useHandleLibrar `opts.getInitialLibraryItems` is deprecated. Use `opts.adapter` instead."
);
Promise.resolve(optsRef.current.getInitialLibraryItems()).then((libraryItems) => {
excalidrawAPI.updateLibrary({
libraryItems,
// merge with current library items because we may have already
// populated it (e.g. by installing 3rd party library which can
// happen before the DB data is loaded)
merge: true
});
}).catch((error) => {
console.error(
`UseHandeLibrary getInitialLibraryItems failed: ${error?.message}`
);
});
}
if ("adapter" in optsRef.current && optsRef.current.adapter) {
const adapter = optsRef.current.adapter;
const migrationAdapter = optsRef.current.migrationAdapter;
const initDataPromise = resolvablePromise();
if (migrationAdapter) {
initDataPromise.resolve(
promiseTry(migrationAdapter.load).then(async (libraryData) => {
let restoredData = null;
try {
if (!libraryData) {
return AdapterTransaction.getLibraryItems(adapter, "load");
}
restoredData = restoreLibraryItems(
libraryData.libraryItems || [],
"published"
);
const nextItems = await persistLibraryUpdate(
adapter,
createLibraryUpdate([], restoredData)
);
try {
await migrationAdapter.clear();
} catch (error) {
console.error(
`couldn't delete legacy library data: ${error.message}`
);
}
return nextItems;
} catch (error) {
console.error(
`couldn't migrate legacy library data: ${error.message}`
);
return restoredData;
}
}).catch((error) => {
console.error(`error during library migration: ${error.message}`);
return AdapterTransaction.getLibraryItems(adapter, "load");
})
);
} else {
initDataPromise.resolve(
promiseTry(AdapterTransaction.getLibraryItems, adapter, "load")
);
}
excalidrawAPI.updateLibrary({
libraryItems: initDataPromise.then((libraryItems) => {
const _libraryItems = libraryItems || [];
lastSavedLibraryItemsHash = getLibraryItemsHash(_libraryItems);
return _libraryItems;
}),
// merge with current library items because we may have already
// populated it (e.g. by installing 3rd party library which can
// happen before the DB data is loaded)
merge: true
}).finally(() => {
isLibraryLoadedRef.current = true;
});
}
window.addEventListener("hashchange" /* HASHCHANGE */, onHashChange);
return () => {
window.removeEventListener("hashchange" /* HASHCHANGE */, onHashChange);
};
}, [
// important this useEffect only depends on excalidrawAPI so it only reruns
// on editor remounts (the excalidrawAPI changes)
excalidrawAPI
]);
useEffect19(
() => {
const unsubOnLibraryUpdate = onLibraryUpdateEmitter.on(
async (update, nextLibraryItems) => {
const isLoaded = isLibraryLoadedRef.current;
const adapter = "adapter" in optsRef.current && optsRef.current.adapter || null;
try {
if (adapter) {
if (
// if nextLibraryItems hash identical to previously saved hash,
// exit early, even if actual upstream state ends up being
// different (e.g. has more data than we have locally), as it'd
// be low-impact scenario.
lastSavedLibraryItemsHash !== getLibraryItemsHash(nextLibraryItems)
) {
await persistLibraryUpdate(adapter, update);
}
}
} catch (error) {
console.error(
`couldn't persist library update: ${error.message}`,
update
);
if (isLoaded && optsRef.current.excalidrawAPI) {
optsRef.current.excalidrawAPI.updateScene({
appState: {
errorMessage: t("errors.saveLibraryError")
}
});
}
}
}
);
const onUnload = (event) => {
if (librarySaveCounter) {
preventUnload(event);
}
};
window.addEventListener("beforeunload" /* BEFORE_UNLOAD */, onUnload);
return () => {
window.removeEventListener("beforeunload" /* BEFORE_UNLOAD */, onUnload);
unsubOnLibraryUpdate();
lastSavedLibraryItemsHash = 0;
librarySaveCounter = 0;
};
},
[
// this effect must not have any deps so it doesn't rerun
]
);
};
// components/LibraryMenuItems.tsx
import {
useCallback as useCallback9,
useEffect as useEffect25,
useMemo as useMemo5,
useRef as useRef18,
useState as useState18
} from "react";
// components/Stack.tsx
import { forwardRef as forwardRef2 } from "react";
import clsx21 from "clsx";
import { jsx as jsx51 } from "react/jsx-runtime";
var RowStack = forwardRef2(
({ children, gap, align, justifyContent, className, style }, ref) => {
return /* @__PURE__ */ jsx51(
"div",
{
className: clsx21("Stack Stack_horizontal", className),
style: {
"--gap": gap,
alignItems: align,
justifyContent,
...style
},
ref,
children
}
);
}
);
var ColStack = forwardRef2(
({ children, gap, align, justifyContent, className, style }, ref) => {
return /* @__PURE__ */ jsx51(
"div",
{
className: clsx21("Stack Stack_vertical", className),
style: {
"--gap": gap,
justifyItems: align,
justifyContent,
...style
},
ref,
children
}
);
}
);
var Stack_default = {
Row: RowStack,
Col: ColStack
};
// components/LibraryMenuBrowseButton.tsx
import { jsx as jsx52 } from "react/jsx-runtime";
var LibraryMenuBrowseButton = ({
theme,
id,
libraryReturnUrl
}) => {
const referrer = libraryReturnUrl || window.location.origin + window.location.pathname;
return /* @__PURE__ */ jsx52(
"a",
{
className: "library-menu-browse-button",
href: `${define_import_meta_env_default.VITE_APP_LIBRARY_URL}?target=${window.name || "_blank"}&referrer=${referrer}&useHash=true&token=${id}&theme=${theme}&version=${VERSIONS.excalidrawLibrary}`,
target: "_excalidraw_libraries",
children: t("labels.libraries")
}
);
};
var LibraryMenuBrowseButton_default = LibraryMenuBrowseButton;
// components/LibraryMenuControlButtons.tsx
import clsx22 from "clsx";
import { jsx as jsx53, jsxs as jsxs28 } from "react/jsx-runtime";
var LibraryMenuControlButtons = ({
libraryReturnUrl,
theme,
id,
style,
children,
className
}) => {
return /* @__PURE__ */ jsxs28(
"div",
{
className: clsx22("library-menu-control-buttons", className),
style,
children: [
/* @__PURE__ */ jsx53(
LibraryMenuBrowseButton_default,
{
id,
libraryReturnUrl,
theme
}
),
children
]
}
);
};
// components/LibraryMenuHeaderContent.tsx
import { useCallback as useCallback7, useState as useState15 } from "react";
// components/Trans.tsx
import React19 from "react";
var SPLIT_REGEX = /({{[\w-]+}})|(<[\w-]+>)|(<\/[\w-]+>)/g;
var KEY_REGEXP = /{{([\w-]+)}}/;
var TAG_START_REGEXP = /<([\w-]+)>/;
var TAG_END_REGEXP = /<\/([\w-]+)>/;
var getTransChildren = (format, props) => {
const stack = [
{
name: "",
children: []
}
];
format.split(SPLIT_REGEX).filter(Boolean).forEach((match) => {
const tagStartMatch = match.match(TAG_START_REGEXP);
const tagEndMatch = match.match(TAG_END_REGEXP);
const keyMatch = match.match(KEY_REGEXP);
if (tagStartMatch !== null) {
const name = tagStartMatch[1];
if (props.hasOwnProperty(name)) {
stack.push({
name,
children: []
});
} else {
console.warn(
`Trans: missed to pass in prop ${name} for interpolating ${format}`
);
}
} else if (tagEndMatch !== null) {
const name = tagEndMatch[1];
if (name === stack[stack.length - 1].name) {
const item = stack.pop();
const itemChildren = React19.createElement(
React19.Fragment,
{},
...item.children
);
const fn = props[item.name];
if (typeof fn === "function") {
stack[stack.length - 1].children.push(fn(itemChildren));
}
} else {
console.warn(
`Trans: unexpected end tag ${match} for interpolating ${format}`
);
}
} else if (keyMatch !== null) {
const name = keyMatch[1];
if (props.hasOwnProperty(name)) {
stack[stack.length - 1].children.push(props[name]);
} else {
console.warn(
`Trans: key ${name} not in props for interpolating ${format}`
);
}
} else {
stack[stack.length - 1].children.push(match);
}
});
if (stack.length !== 1) {
console.warn(`Trans: stack not empty for interpolating ${format}`);
}
return stack[0].children;
};
var Trans = ({
i18nKey,
children,
...props
}) => {
const { t: t2 } = useI18n();
return React19.createElement(
React19.Fragment,
{},
...getTransChildren(t2(i18nKey), props)
);
};
var Trans_default = Trans;
// components/ConfirmDialog.tsx
import { flushSync } from "react-dom";
// components/DialogActionButton.tsx
import clsx23 from "clsx";
import { jsx as jsx54, jsxs as jsxs29 } from "react/jsx-runtime";
var DialogActionButton = ({
label,
onClick,
className,
children,
actionType,
type = "button",
isLoading,
...rest
}) => {
const cs = actionType ? `Dialog__action-button--${actionType}` : "";
return /* @__PURE__ */ jsxs29(
"button",
{
className: clsx23("Dialog__action-button", cs, className),
type,
"aria-label": label,
onClick,
...rest,
children: [
children && /* @__PURE__ */ jsx54("div", { style: isLoading ? { visibility: "hidden" } : {}, children }),
/* @__PURE__ */ jsx54("div", { style: isLoading ? { visibility: "hidden" } : {}, children: label }),
isLoading && /* @__PURE__ */ jsx54("div", { style: { position: "absolute", inset: 0 }, children: /* @__PURE__ */ jsx54(Spinner_default, {}) })
]
}
);
};
var DialogActionButton_default = DialogActionButton;
// components/ConfirmDialog.tsx
import { jsx as jsx55, jsxs as jsxs30 } from "react/jsx-runtime";
var ConfirmDialog = (props) => {
const {
onConfirm,
onCancel,
children,
confirmText = t("buttons.confirm"),
cancelText = t("buttons.cancel"),
className = "",
...rest
} = props;
const setAppState = useExcalidrawSetAppState();
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
const { container } = useExcalidrawContainer();
return /* @__PURE__ */ jsxs30(
Dialog,
{
onCloseRequest: onCancel,
size: "small",
...rest,
className: `confirm-dialog ${className}`,
children: [
children,
/* @__PURE__ */ jsxs30("div", { className: "confirm-dialog-buttons", children: [
/* @__PURE__ */ jsx55(
DialogActionButton_default,
{
label: cancelText,
onClick: () => {
setAppState({ openMenu: null });
setIsLibraryMenuOpen(false);
flushSync(() => {
onCancel();
});
container?.focus();
}
}
),
/* @__PURE__ */ jsx55(
DialogActionButton_default,
{
label: confirmText,
onClick: () => {
setAppState({ openMenu: null });
setIsLibraryMenuOpen(false);
flushSync(() => {
onConfirm();
});
container?.focus();
},
actionType: "danger"
}
)
] })
]
}
);
};
var ConfirmDialog_default = ConfirmDialog;
// components/PublishLibrary.tsx
import { useCallback as useCallback6, useEffect as useEffect20, useRef as useRef15, useState as useState14 } from "react";
import OpenColor from "open-color";
// data/EditorLocalStorage.ts
var EditorLocalStorage = class {
static has(key) {
try {
return !!window.localStorage.getItem(key);
} catch (error) {
console.warn(`localStorage.getItem error: ${error.message}`);
return false;
}
}
static get(key) {
try {
const value = window.localStorage.getItem(key);
if (value) {
return JSON.parse(value);
}
return null;
} catch (error) {
console.warn(`localStorage.getItem error: ${error.message}`);
return null;
}
}
};
__publicField(EditorLocalStorage, "set", (key, value) => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (error) {
console.warn(`localStorage.setItem error: ${error.message}`);
return false;
}
});
__publicField(EditorLocalStorage, "delete", (name) => {
try {
window.localStorage.removeItem(name);
} catch (error) {
console.warn(`localStorage.removeItem error: ${error.message}`);
}
});
// components/PublishLibrary.tsx
import { jsx as jsx56, jsxs as jsxs31 } from "react/jsx-runtime";
var generatePreviewImage = async (libraryItems) => {
const MAX_ITEMS_PER_ROW = 6;
const BOX_SIZE = 128;
const BOX_PADDING = Math.round(BOX_SIZE / 16);
const BORDER_WIDTH = Math.max(Math.round(BOX_SIZE / 64), 2);
const rows = chunk(libraryItems, MAX_ITEMS_PER_ROW);
const canvas = document.createElement("canvas");
canvas.width = rows[0].length * BOX_SIZE + (rows[0].length + 1) * (BOX_PADDING * 2) - BOX_PADDING * 2;
canvas.height = rows.length * BOX_SIZE + (rows.length + 1) * (BOX_PADDING * 2) - BOX_PADDING * 2;
const ctx = canvas.getContext("2d");
ctx.fillStyle = OpenColor.white;
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (const [index, item] of libraryItems.entries()) {
const itemCanvas = await exportToCanvas2({
elements: item.elements,
files: null,
maxWidthOrHeight: BOX_SIZE
});
const { width, height } = itemCanvas;
const rowOffset = Math.floor(index / MAX_ITEMS_PER_ROW) * (BOX_SIZE + BOX_PADDING * 2);
const colOffset = index % MAX_ITEMS_PER_ROW * (BOX_SIZE + BOX_PADDING * 2);
ctx.drawImage(
itemCanvas,
colOffset + (BOX_SIZE - width) / 2 + BOX_PADDING,
rowOffset + (BOX_SIZE - height) / 2 + BOX_PADDING
);
ctx.lineWidth = BORDER_WIDTH;
ctx.strokeStyle = OpenColor.gray[4];
ctx.strokeRect(
colOffset + BOX_PADDING / 2,
rowOffset + BOX_PADDING / 2,
BOX_SIZE + BOX_PADDING,
BOX_SIZE + BOX_PADDING
);
}
return await resizeImageFile(
new File([await canvasToBlob(canvas)], "preview", { type: MIME_TYPES.png }),
{
outputType: MIME_TYPES.jpg,
maxWidthOrHeight: 5e3
}
);
};
var SingleLibraryItem = ({
libItem,
appState,
index,
onChange,
onRemove
}) => {
const svgRef = useRef15(null);
const inputRef = useRef15(null);
useEffect20(() => {
const node = svgRef.current;
if (!node) {
return;
}
(async () => {
const svg = await exportToSvg2({
elements: libItem.elements,
appState: {
...appState,
viewBackgroundColor: OpenColor.white,
exportBackground: true
},
files: null,
skipInliningFonts: true
});
node.innerHTML = svg.outerHTML;
})();
}, [libItem.elements, appState]);
return /* @__PURE__ */ jsxs31("div", { className: "single-library-item", children: [
libItem.status === "published" && /* @__PURE__ */ jsx56("span", { className: "single-library-item-status", children: t("labels.statusPublished") }),
/* @__PURE__ */ jsx56("div", { ref: svgRef, className: "single-library-item__svg" }),
/* @__PURE__ */ jsx56(
ToolButton,
{
"aria-label": t("buttons.remove"),
type: "button",
icon: CloseIcon,
className: "single-library-item--remove",
onClick: onRemove.bind(null, libItem.id),
title: t("buttons.remove")
}
),
/* @__PURE__ */ jsxs31(
"div",
{
style: {
display: "flex",
margin: "0.8rem 0",
width: "100%",
fontSize: "14px",
fontWeight: 500,
flexDirection: "column"
},
children: [
/* @__PURE__ */ jsxs31(
"label",
{
style: {
display: "flex",
justifyContent: "space-between",
flexDirection: "column"
},
children: [
/* @__PURE__ */ jsxs31("div", { style: { padding: "0.5em 0" }, children: [
/* @__PURE__ */ jsx56("span", { style: { fontWeight: 500, color: OpenColor.gray[6] }, children: t("publishDialog.itemName") }),
/* @__PURE__ */ jsx56("span", { "aria-hidden": "true", className: "required", children: "*" })
] }),
/* @__PURE__ */ jsx56(
"input",
{
type: "text",
ref: inputRef,
style: { width: "80%", padding: "0.2rem" },
defaultValue: libItem.name,
placeholder: "Item name",
onChange: (event) => {
onChange(event.target.value, index);
}
}
)
]
}
),
/* @__PURE__ */ jsx56("span", { className: "error", children: libItem.error })
]
}
)
] });
};
var PublishLibrary = ({
onClose,
libraryItems,
appState,
onSuccess,
onError,
updateItemsInStorage,
onRemove
}) => {
const [libraryData, setLibraryData] = useState14({
authorName: "",
githubHandle: "",
name: "",
description: "",
twitterHandle: "",
website: ""
});
const [isSubmitting, setIsSubmitting] = useState14(false);
useEffect20(() => {
const data = EditorLocalStorage.get(
EDITOR_LS_KEYS.PUBLISH_LIBRARY
);
if (data) {
setLibraryData(data);
}
}, []);
const [clonedLibItems, setClonedLibItems] = useState14(
libraryItems.slice()
);
useEffect20(() => {
setClonedLibItems(libraryItems.slice());
}, [libraryItems]);
const onInputChange = (event) => {
setLibraryData({
...libraryData,
[event.target.name]: event.target.value
});
};
const onSubmit = async (event) => {
event.preventDefault();
setIsSubmitting(true);
const erroredLibItems = [];
let isError = false;
clonedLibItems.forEach((libItem) => {
let error = "";
if (!libItem.name) {
error = t("publishDialog.errors.required");
isError = true;
}
erroredLibItems.push({ ...libItem, error });
});
if (isError) {
setClonedLibItems(erroredLibItems);
setIsSubmitting(false);
return;
}
const previewImage = await generatePreviewImage(clonedLibItems);
const libContent = {
type: EXPORT_DATA_TYPES.excalidrawLibrary,
version: VERSIONS.excalidrawLibrary,
source: EXPORT_SOURCE,
libraryItems: clonedLibItems
};
const content = JSON.stringify(libContent, null, 2);
const lib = new Blob([content], { type: "application/json" });
const formData = new FormData();
formData.append("excalidrawLib", lib);
formData.append("previewImage", previewImage);
formData.append("previewImageType", previewImage.type);
formData.append("title", libraryData.name);
formData.append("authorName", libraryData.authorName);
formData.append("githubHandle", libraryData.githubHandle);
formData.append("name", libraryData.name);
formData.append("description", libraryData.description);
formData.append("twitterHandle", libraryData.twitterHandle);
formData.append("website", libraryData.website);
fetch(`${define_import_meta_env_default.VITE_APP_LIBRARY_BACKEND}/submit`, {
method: "post",
body: formData
}).then(
(response) => {
if (response.ok) {
return response.json().then(({ url }) => {
EditorLocalStorage.delete(EDITOR_LS_KEYS.PUBLISH_LIBRARY);
onSuccess({
url,
authorName: libraryData.authorName,
items: clonedLibItems
});
});
}
return response.json().catch(() => {
throw new Error(response.statusText || "something went wrong");
}).then((error) => {
throw new Error(
error.message || response.statusText || "something went wrong"
);
});
},
(err) => {
console.error(err);
onError(err);
setIsSubmitting(false);
}
).catch((err) => {
console.error(err);
onError(err);
setIsSubmitting(false);
});
};
const renderLibraryItems = () => {
const items = [];
clonedLibItems.forEach((libItem, index) => {
items.push(
/* @__PURE__ */ jsx56("div", { className: "single-library-item-wrapper", children: /* @__PURE__ */ jsx56(
SingleLibraryItem,
{
libItem,
appState,
index,
onChange: (val, index2) => {
const items2 = clonedLibItems.slice();
items2[index2].name = val;
setClonedLibItems(items2);
},
onRemove
}
) }, index)
);
});
return /* @__PURE__ */ jsx56("div", { className: "selected-library-items", children: items });
};
const onDialogClose = useCallback6(() => {
updateItemsInStorage(clonedLibItems);
EditorLocalStorage.set(EDITOR_LS_KEYS.PUBLISH_LIBRARY, libraryData);
onClose();
}, [clonedLibItems, onClose, updateItemsInStorage, libraryData]);
const shouldRenderForm = !!libraryItems.length;
const containsPublishedItems = libraryItems.some(
(item) => item.status === "published"
);
return /* @__PURE__ */ jsx56(
Dialog,
{
onCloseRequest: onDialogClose,
title: t("publishDialog.title"),
className: "publish-library",
children: shouldRenderForm ? /* @__PURE__ */ jsxs31("form", { onSubmit, children: [
/* @__PURE__ */ jsx56("div", { className: "publish-library-note", children: /* @__PURE__ */ jsx56(
Trans_default,
{
i18nKey: "publishDialog.noteDescription",
link: (el) => /* @__PURE__ */ jsx56(
"a",
{
href: "https://libraries.excalidraw.com",
target: "_blank",
rel: "noopener noreferrer",
children: el
}
)
}
) }),
/* @__PURE__ */ jsx56("span", { className: "publish-library-note", children: /* @__PURE__ */ jsx56(
Trans_default,
{
i18nKey: "publishDialog.noteGuidelines",
link: (el) => /* @__PURE__ */ jsx56(
"a",
{
href: "https://github.com/excalidraw/excalidraw-libraries#guidelines",
target: "_blank",
rel: "noopener noreferrer",
children: el
}
)
}
) }),
/* @__PURE__ */ jsx56("div", { className: "publish-library-note", children: t("publishDialog.noteItems") }),
containsPublishedItems && /* @__PURE__ */ jsx56("span", { className: "publish-library-note publish-library-warning", children: t("publishDialog.republishWarning") }),
renderLibraryItems(),
/* @__PURE__ */ jsxs31("div", { className: "publish-library__fields", children: [
/* @__PURE__ */ jsxs31("label", { children: [
/* @__PURE__ */ jsxs31("div", { children: [
/* @__PURE__ */ jsx56("span", { children: t("publishDialog.libraryName") }),
/* @__PURE__ */ jsx56("span", { "aria-hidden": "true", className: "required", children: "*" })
] }),
/* @__PURE__ */ jsx56(
"input",
{
type: "text",
name: "name",
required: true,
value: libraryData.name,
onChange: onInputChange,
placeholder: t("publishDialog.placeholder.libraryName")
}
)
] }),
/* @__PURE__ */ jsxs31("label", { style: { alignItems: "flex-start" }, children: [
/* @__PURE__ */ jsxs31("div", { children: [
/* @__PURE__ */ jsx56("span", { children: t("publishDialog.libraryDesc") }),
/* @__PURE__ */ jsx56("span", { "aria-hidden": "true", className: "required", children: "*" })
] }),
/* @__PURE__ */ jsx56(
"textarea",
{
name: "description",
rows: 4,
required: true,
value: libraryData.description,
onChange: onInputChange,
placeholder: t("publishDialog.placeholder.libraryDesc")
}
)
] }),
/* @__PURE__ */ jsxs31("label", { children: [
/* @__PURE__ */ jsxs31("div", { children: [
/* @__PURE__ */ jsx56("span", { children: t("publishDialog.authorName") }),
/* @__PURE__ */ jsx56("span", { "aria-hidden": "true", className: "required", children: "*" })
] }),
/* @__PURE__ */ jsx56(
"input",
{
type: "text",
name: "authorName",
required: true,
value: libraryData.authorName,
onChange: onInputChange,
placeholder: t("publishDialog.placeholder.authorName")
}
)
] }),
/* @__PURE__ */ jsxs31("label", { children: [
/* @__PURE__ */ jsx56("span", { children: t("publishDialog.githubUsername") }),
/* @__PURE__ */ jsx56(
"input",
{
type: "text",
name: "githubHandle",
value: libraryData.githubHandle,
onChange: onInputChange,
placeholder: t("publishDialog.placeholder.githubHandle")
}
)
] }),
/* @__PURE__ */ jsxs31("label", { children: [
/* @__PURE__ */ jsx56("span", { children: t("publishDialog.twitterUsername") }),
/* @__PURE__ */ jsx56(
"input",
{
type: "text",
name: "twitterHandle",
value: libraryData.twitterHandle,
onChange: onInputChange,
placeholder: t("publishDialog.placeholder.twitterHandle")
}
)
] }),
/* @__PURE__ */ jsxs31("label", { children: [
/* @__PURE__ */ jsx56("span", { children: t("publishDialog.website") }),
/* @__PURE__ */ jsx56(
"input",
{
type: "text",
name: "website",
pattern: "https?://.+",
title: t("publishDialog.errors.website"),
value: libraryData.website,
onChange: onInputChange,
placeholder: t("publishDialog.placeholder.website")
}
)
] }),
/* @__PURE__ */ jsx56("span", { className: "publish-library-note", children: /* @__PURE__ */ jsx56(
Trans_default,
{
i18nKey: "publishDialog.noteLicense",
link: (el) => /* @__PURE__ */ jsx56(
"a",
{
href: "https://github.com/excalidraw/excalidraw-libraries/blob/main/LICENSE",
target: "_blank",
rel: "noopener noreferrer",
children: el
}
)
}
) })
] }),
/* @__PURE__ */ jsxs31("div", { className: "publish-library__buttons", children: [
/* @__PURE__ */ jsx56(
DialogActionButton_default,
{
label: t("buttons.cancel"),
onClick: onDialogClose,
"data-testid": "cancel-clear-canvas-button"
}
),
/* @__PURE__ */ jsx56(
DialogActionButton_default,
{
type: "submit",
label: t("buttons.submit"),
actionType: "primary",
isLoading: isSubmitting
}
)
] })
] }) : /* @__PURE__ */ jsx56("p", { style: { padding: "1em", textAlign: "center", fontWeight: 500 }, children: t("publishDialog.atleastOneLibItem") })
}
);
};
var PublishLibrary_default = PublishLibrary;
// components/dropdownMenu/DropdownMenuTrigger.tsx
import clsx24 from "clsx";
import { jsx as jsx57 } from "react/jsx-runtime";
var MenuTrigger = ({
className = "",
children,
onToggle,
title,
...rest
}) => {
const device = useDevice();
const classNames = clsx24(
`dropdown-menu-button ${className}`,
"zen-mode-transition",
{
"dropdown-menu-button--mobile": device.editor.isMobile
}
).trim();
return /* @__PURE__ */ jsx57(
"button",
{
"data-prevent-outside-click": true,
className: classNames,
onClick: onToggle,
type: "button",
"data-testid": "dropdown-menu-button",
title,
...rest,
children
}
);
};
var DropdownMenuTrigger_default = MenuTrigger;
MenuTrigger.displayName = "DropdownMenuTrigger";
// components/dropdownMenu/DropdownMenuSeparator.tsx
import { jsx as jsx58 } from "react/jsx-runtime";
var MenuSeparator = () => /* @__PURE__ */ jsx58(
"div",
{
style: {
height: "1px",
backgroundColor: "var(--default-border-color)",
margin: ".5rem 0"
}
}
);
var DropdownMenuSeparator_default = MenuSeparator;
MenuSeparator.displayName = "DropdownMenuSeparator";
// components/dropdownMenu/DropdownMenuContent.tsx
import clsx25 from "clsx";
import { useEffect as useEffect21, useRef as useRef16 } from "react";
import { jsx as jsx59 } from "react/jsx-runtime";
var MenuContent = ({
children,
onClickOutside,
className = "",
onSelect,
style
}) => {
const device = useDevice();
const menuRef = useRef16(null);
const callbacksRef = useStable({ onClickOutside });
useOutsideClick(menuRef, () => {
callbacksRef.onClickOutside?.();
});
useEffect21(() => {
const onKeyDown = (event) => {
if (event.key === KEYS.ESCAPE) {
event.stopImmediatePropagation();
callbacksRef.onClickOutside?.();
}
};
const option = {
// so that we can stop propagation of the event before it reaches
// event handlers that were bound before this one
capture: true
};
document.addEventListener("keydown" /* KEYDOWN */, onKeyDown, option);
return () => {
document.removeEventListener("keydown" /* KEYDOWN */, onKeyDown, option);
};
}, [callbacksRef]);
const classNames = clsx25(`dropdown-menu ${className}`, {
"dropdown-menu--mobile": device.editor.isMobile
}).trim();
return /* @__PURE__ */ jsx59(DropdownMenuContentPropsContext.Provider, { value: { onSelect }, children: /* @__PURE__ */ jsx59(
"div",
{
ref: menuRef,
className: classNames,
style,
"data-testid": "dropdown-menu",
children: device.editor.isMobile ? /* @__PURE__ */ jsx59(Stack_default.Col, { className: "dropdown-menu-container", children }) : /* @__PURE__ */ jsx59(
Island,
{
className: "dropdown-menu-container",
padding: 2,
style: { zIndex: 2 },
children
}
)
}
) });
};
MenuContent.displayName = "DropdownMenuContent";
var DropdownMenuContent_default = MenuContent;
// components/dropdownMenu/DropdownMenuItemLink.tsx
import { jsx as jsx60 } from "react/jsx-runtime";
var DropdownMenuItemLink = ({
icon,
shortcut,
href,
children,
onSelect,
className = "",
selected,
rel = "noreferrer",
...rest
}) => {
const handleClick = useHandleDropdownMenuItemClick(rest.onClick, onSelect);
return /* @__PURE__ */ jsx60(
"a",
{
...rest,
href,
target: "_blank",
rel: "noreferrer",
className: getDropdownMenuItemClassName(className, selected),
title: rest.title ?? rest["aria-label"],
onClick: handleClick,
children: /* @__PURE__ */ jsx60(DropdownMenuItemContent_default, { icon, shortcut, children })
}
);
};
var DropdownMenuItemLink_default = DropdownMenuItemLink;
DropdownMenuItemLink.displayName = "DropdownMenuItemLink";
// components/dropdownMenu/DropdownMenuItemCustom.tsx
import { jsx as jsx61 } from "react/jsx-runtime";
var DropdownMenuItemCustom = ({
children,
className = "",
selected,
...rest
}) => {
return /* @__PURE__ */ jsx61(
"div",
{
...rest,
className: `dropdown-menu-item-base dropdown-menu-item-custom ${className} ${selected ? `dropdown-menu-item--selected` : ``}`.trim(),
children
}
);
};
var DropdownMenuItemCustom_default = DropdownMenuItemCustom;
// components/dropdownMenu/dropdownMenuUtils.ts
import React21 from "react";
var getMenuTriggerComponent = (children) => {
const comp = React21.Children.toArray(children).find(
(child) => React21.isValidElement(child) && typeof child.type !== "string" && //@ts-ignore
child?.type.displayName && //@ts-ignore
child.type.displayName === "DropdownMenuTrigger"
);
if (!comp) {
return null;
}
return comp;
};
var getMenuContentComponent = (children) => {
const comp = React21.Children.toArray(children).find(
(child) => React21.isValidElement(child) && typeof child.type !== "string" && //@ts-ignore
child?.type.displayName && //@ts-ignore
child.type.displayName === "DropdownMenuContent"
);
if (!comp) {
return null;
}
return comp;
};
// components/dropdownMenu/DropdownMenu.tsx
import { Fragment as Fragment6, jsxs as jsxs32 } from "react/jsx-runtime";
var DropdownMenu = ({
children,
open
}) => {
const MenuTriggerComp = getMenuTriggerComponent(children);
const MenuContentComp = getMenuContentComponent(children);
return /* @__PURE__ */ jsxs32(Fragment6, { children: [
MenuTriggerComp,
open && MenuContentComp
] });
};
DropdownMenu.Trigger = DropdownMenuTrigger_default;
DropdownMenu.Content = DropdownMenuContent_default;
DropdownMenu.Item = DropdownMenuItem_default;
DropdownMenu.ItemLink = DropdownMenuItemLink_default;
DropdownMenu.ItemCustom = DropdownMenuItemCustom_default;
DropdownMenu.Group = DropdownMenuGroup_default;
DropdownMenu.Separator = DropdownMenuSeparator_default;
var DropdownMenu_default = DropdownMenu;
DropdownMenu.displayName = "DropdownMenu";
// components/LibraryMenuHeaderContent.tsx
import clsx26 from "clsx";
import { jsx as jsx62, jsxs as jsxs33 } from "react/jsx-runtime";
var getSelectedItems = (libraryItems, selectedItems) => libraryItems.filter((item) => selectedItems.includes(item.id));
var LibraryDropdownMenuButton = ({
setAppState,
selectedItems,
library,
onRemoveFromLibrary,
resetLibrary,
onSelectItems,
appState,
className
}) => {
const [libraryItemsData] = useAtom(libraryItemsAtom);
const [isLibraryMenuOpen, setIsLibraryMenuOpen] = useAtom(
isLibraryMenuOpenAtom
);
const renderRemoveLibAlert = () => {
const content = selectedItems.length ? t("alerts.removeItemsFromsLibrary", { count: selectedItems.length }) : t("alerts.resetLibrary");
const title = selectedItems.length ? t("confirmDialog.removeItemsFromLib") : t("confirmDialog.resetLibrary");
return /* @__PURE__ */ jsx62(
ConfirmDialog_default,
{
onConfirm: () => {
if (selectedItems.length) {
onRemoveFromLibrary();
} else {
resetLibrary();
}
setShowRemoveLibAlert(false);
},
onCancel: () => {
setShowRemoveLibAlert(false);
},
title,
children: /* @__PURE__ */ jsx62("p", { children: content })
}
);
};
const [showRemoveLibAlert, setShowRemoveLibAlert] = useState15(false);
const itemsSelected = !!selectedItems.length;
const items = itemsSelected ? libraryItemsData.libraryItems.filter(
(item) => selectedItems.includes(item.id)
) : libraryItemsData.libraryItems;
const resetLabel = itemsSelected ? t("buttons.remove") : t("buttons.resetLibrary");
const [showPublishLibraryDialog, setShowPublishLibraryDialog] = useState15(false);
const [publishLibSuccess, setPublishLibSuccess] = useState15(null);
const renderPublishSuccess = useCallback7(() => {
return /* @__PURE__ */ jsxs33(
Dialog,
{
onCloseRequest: () => setPublishLibSuccess(null),
title: t("publishSuccessDialog.title"),
className: "publish-library-success",
size: "small",
children: [
/* @__PURE__ */ jsx62("p", { children: /* @__PURE__ */ jsx62(
Trans_default,
{
i18nKey: "publishSuccessDialog.content",
authorName: publishLibSuccess.authorName,
link: (el) => /* @__PURE__ */ jsx62(
"a",
{
href: publishLibSuccess?.url,
target: "_blank",
rel: "noopener noreferrer",
children: el
}
)
}
) }),
/* @__PURE__ */ jsx62(
ToolButton,
{
type: "button",
title: t("buttons.close"),
"aria-label": t("buttons.close"),
label: t("buttons.close"),
onClick: () => setPublishLibSuccess(null),
"data-testid": "publish-library-success-close",
className: "publish-library-success-close"
}
)
]
}
);
}, [setPublishLibSuccess, publishLibSuccess]);
const onPublishLibSuccess = (data, libraryItems) => {
setShowPublishLibraryDialog(false);
setPublishLibSuccess({ url: data.url, authorName: data.authorName });
const nextLibItems = libraryItems.slice();
nextLibItems.forEach((libItem) => {
if (selectedItems.includes(libItem.id)) {
libItem.status = "published";
}
});
library.setLibrary(nextLibItems);
};
const onLibraryImport = async () => {
try {
await library.updateLibrary({
libraryItems: fileOpen({
description: "Excalidraw library files"
// ToDo: Be over-permissive until https://bugs.webkit.org/show_bug.cgi?id=34442
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
/*
extensions: [".json", ".excalidrawlib"],
*/
}),
merge: true,
openLibraryMenu: true
});
} catch (error) {
if (error?.name === "AbortError") {
console.warn(error);
return;
}
setAppState({ errorMessage: t("errors.importLibraryError") });
}
};
const onLibraryExport = async () => {
const libraryItems = itemsSelected ? items : await library.getLatestLibrary();
saveLibraryAsJSON(libraryItems).catch(muteFSAbortError).catch((error) => {
setAppState({ errorMessage: error.message });
});
};
const renderLibraryMenu = () => {
return /* @__PURE__ */ jsxs33(DropdownMenu_default, { open: isLibraryMenuOpen, children: [
/* @__PURE__ */ jsx62(
DropdownMenu_default.Trigger,
{
onToggle: () => setIsLibraryMenuOpen(!isLibraryMenuOpen),
children: DotsIcon
}
),
/* @__PURE__ */ jsxs33(
DropdownMenu_default.Content,
{
onClickOutside: () => setIsLibraryMenuOpen(false),
onSelect: () => setIsLibraryMenuOpen(false),
className: "library-menu",
children: [
!itemsSelected && /* @__PURE__ */ jsx62(
DropdownMenu_default.Item,
{
onSelect: onLibraryImport,
icon: LoadIcon,
"data-testid": "lib-dropdown--load",
children: t("buttons.load")
}
),
!!items.length && /* @__PURE__ */ jsx62(
DropdownMenu_default.Item,
{
onSelect: onLibraryExport,
icon: ExportIcon,
"data-testid": "lib-dropdown--export",
children: t("buttons.export")
}
),
!!items.length && /* @__PURE__ */ jsx62(
DropdownMenu_default.Item,
{
onSelect: () => setShowRemoveLibAlert(true),
icon: TrashIcon,
children: resetLabel
}
),
itemsSelected && /* @__PURE__ */ jsx62(
DropdownMenu_default.Item,
{
icon: publishIcon,
onSelect: () => setShowPublishLibraryDialog(true),
"data-testid": "lib-dropdown--remove",
children: t("buttons.publishLibrary")
}
)
]
}
)
] });
};
return /* @__PURE__ */ jsxs33("div", { className: clsx26("library-menu-dropdown-container", className), children: [
renderLibraryMenu(),
selectedItems.length > 0 && /* @__PURE__ */ jsx62("div", { className: "library-actions-counter", children: selectedItems.length }),
showRemoveLibAlert && renderRemoveLibAlert(),
showPublishLibraryDialog && /* @__PURE__ */ jsx62(
PublishLibrary_default,
{
onClose: () => setShowPublishLibraryDialog(false),
libraryItems: getSelectedItems(
libraryItemsData.libraryItems,
selectedItems
),
appState,
onSuccess: (data) => onPublishLibSuccess(data, libraryItemsData.libraryItems),
onError: (error) => window.alert(error),
updateItemsInStorage: () => library.setLibrary(libraryItemsData.libraryItems),
onRemove: (id) => onSelectItems(selectedItems.filter((_id) => _id !== id))
}
),
publishLibSuccess && renderPublishSuccess()
] });
};
var LibraryDropdownMenu = ({
selectedItems,
onSelectItems,
className
}) => {
const { library } = useApp();
const { clearLibraryCache, deleteItemsFromLibraryCache } = useLibraryCache();
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const [libraryItemsData] = useAtom(libraryItemsAtom);
const removeFromLibrary = async (libraryItems) => {
const nextItems = libraryItems.filter(
(item) => !selectedItems.includes(item.id)
);
library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorRemovingFromLibrary") });
});
deleteItemsFromLibraryCache(selectedItems);
onSelectItems([]);
};
const resetLibrary = () => {
library.resetLibrary();
clearLibraryCache();
};
return /* @__PURE__ */ jsx62(
LibraryDropdownMenuButton,
{
appState,
setAppState,
selectedItems,
onSelectItems,
library,
onRemoveFromLibrary: () => removeFromLibrary(libraryItemsData.libraryItems),
resetLibrary,
className
}
);
};
// components/LibraryMenuSection.tsx
import { memo as memo2, useEffect as useEffect23, useState as useState17 } from "react";
// components/LibraryUnit.tsx
import clsx27 from "clsx";
import { memo, useEffect as useEffect22, useRef as useRef17, useState as useState16 } from "react";
import { jsx as jsx63, jsxs as jsxs34 } from "react/jsx-runtime";
var LibraryUnit = memo(
({
id,
elements,
isPending,
onClick,
selected,
onToggle,
onDrag,
svgCache
}) => {
const ref = useRef17(null);
const svg = useLibraryItemSvg(id, elements, svgCache);
useEffect22(() => {
const node = ref.current;
if (!node) {
return;
}
if (svg) {
node.innerHTML = svg.outerHTML;
}
return () => {
node.innerHTML = "";
};
}, [svg]);
const [isHovered, setIsHovered] = useState16(false);
const isMobile = useDevice().editor.isMobile;
const adder = isPending && /* @__PURE__ */ jsx63("div", { className: "library-unit__adder", children: PlusIcon });
return /* @__PURE__ */ jsxs34(
"div",
{
className: clsx27("library-unit", {
"library-unit__active": elements,
"library-unit--hover": elements && isHovered,
"library-unit--selected": selected,
"library-unit--skeleton": !svg
}),
onMouseEnter: () => setIsHovered(true),
onMouseLeave: () => setIsHovered(false),
children: [
/* @__PURE__ */ jsx63(
"div",
{
className: clsx27("library-unit__dragger", {
"library-unit__pulse": !!isPending
}),
ref,
draggable: !!elements,
onClick: !!elements || !!isPending ? (event) => {
if (id && event.shiftKey) {
onToggle(id, event);
} else {
onClick(id);
}
} : void 0,
onDragStart: (event) => {
if (!id) {
event.preventDefault();
return;
}
setIsHovered(false);
onDrag(id, event);
}
}
),
adder,
id && elements && (isHovered || isMobile || selected) && /* @__PURE__ */ jsx63(
CheckboxItem,
{
checked: selected,
onChange: (checked, event) => onToggle(id, event),
className: "library-unit__checkbox"
}
)
]
}
);
}
);
var EmptyLibraryUnit = () => /* @__PURE__ */ jsx63("div", { className: "library-unit library-unit--skeleton" });
// hooks/useTransition.ts
import React22, { useCallback as useCallback8 } from "react";
function useTransitionPolyfill() {
const startTransition = useCallback8((callback) => callback(), []);
return [false, startTransition];
}
var useTransition = React22.useTransition || useTransitionPolyfill;
// components/LibraryMenuSection.tsx
import { Fragment as Fragment7, jsx as jsx64 } from "react/jsx-runtime";
var LibraryMenuSectionGrid = ({
children
}) => {
return /* @__PURE__ */ jsx64("div", { className: "library-menu-items-container__grid", children });
};
var LibraryMenuSection = memo2(
({
items,
onItemSelectToggle,
onItemDrag,
isItemSelected,
onClick,
svgCache,
itemsRenderedPerBatch
}) => {
const [, startTransition] = useTransition();
const [index, setIndex] = useState17(0);
useEffect23(() => {
if (index < items.length) {
startTransition(() => {
setIndex(index + itemsRenderedPerBatch);
});
}
}, [index, items.length, startTransition, itemsRenderedPerBatch]);
return /* @__PURE__ */ jsx64(Fragment7, { children: items.map((item, i) => {
return i < index ? /* @__PURE__ */ jsx64(
LibraryUnit,
{
elements: item?.elements,
isPending: !item?.id && !!item?.elements,
onClick,
svgCache,
id: item?.id,
selected: isItemSelected(item.id),
onToggle: onItemSelectToggle,
onDrag: onItemDrag
},
item?.id ?? i
) : /* @__PURE__ */ jsx64(EmptyLibraryUnit, {}, i);
}) });
}
);
// hooks/useScrollPosition.ts
import { useEffect as useEffect24 } from "react";
import throttle from "lodash.throttle";
var scrollPositionAtom = atom(0);
var useScrollPosition = (elementRef) => {
const [scrollPosition, setScrollPosition] = useAtom(scrollPositionAtom);
useEffect24(() => {
const { current: element } = elementRef;
if (!element) {
return;
}
const handleScroll = throttle(() => {
const { scrollTop } = element;
setScrollPosition(scrollTop);
}, 200);
element.addEventListener("scroll", handleScroll);
return () => {
handleScroll.cancel();
element.removeEventListener("scroll", handleScroll);
};
}, [elementRef, setScrollPosition]);
return scrollPosition;
};
// components/LibraryMenuItems.tsx
import { Fragment as Fragment8, jsx as jsx65, jsxs as jsxs35 } from "react/jsx-runtime";
var ITEMS_RENDERED_PER_BATCH = 17;
var CACHED_ITEMS_RENDERED_PER_BATCH = 64;
function LibraryMenuItems({
isLoading,
libraryItems,
onAddToLibrary,
onInsertLibraryItems,
pendingElements,
theme,
id,
libraryReturnUrl,
onSelectItems,
selectedItems
}) {
const libraryContainerRef = useRef18(null);
const scrollPosition = useScrollPosition(libraryContainerRef);
useEffect25(() => {
if (scrollPosition > 0) {
libraryContainerRef.current?.scrollTo(0, scrollPosition);
}
}, []);
const { svgCache } = useLibraryCache();
const unpublishedItems = useMemo5(
() => libraryItems.filter((item) => item.status !== "published"),
[libraryItems]
);
const publishedItems = useMemo5(
() => libraryItems.filter((item) => item.status === "published"),
[libraryItems]
);
const showBtn = !libraryItems.length && !pendingElements.length;
const isLibraryEmpty = !pendingElements.length && !unpublishedItems.length && !publishedItems.length;
const [lastSelectedItem, setLastSelectedItem] = useState18(null);
const onItemSelectToggle = useCallback9(
(id2, event) => {
const shouldSelect = !selectedItems.includes(id2);
const orderedItems = [...unpublishedItems, ...publishedItems];
if (shouldSelect) {
if (event.shiftKey && lastSelectedItem) {
const rangeStart = orderedItems.findIndex(
(item) => item.id === lastSelectedItem
);
const rangeEnd = orderedItems.findIndex((item) => item.id === id2);
if (rangeStart === -1 || rangeEnd === -1) {
onSelectItems([...selectedItems, id2]);
return;
}
const selectedItemsMap = arrayToMap(selectedItems);
const nextSelectedIds = orderedItems.reduce(
(acc, item, idx) => {
if (idx >= rangeStart && idx <= rangeEnd || selectedItemsMap.has(item.id)) {
acc.push(item.id);
}
return acc;
},
[]
);
onSelectItems(nextSelectedIds);
} else {
onSelectItems([...selectedItems, id2]);
}
setLastSelectedItem(id2);
} else {
setLastSelectedItem(null);
onSelectItems(selectedItems.filter((_id) => _id !== id2));
}
},
[
lastSelectedItem,
onSelectItems,
publishedItems,
selectedItems,
unpublishedItems
]
);
const getInsertedElements = useCallback9(
(id2) => {
let targetElements;
if (selectedItems.includes(id2)) {
targetElements = libraryItems.filter(
(item) => selectedItems.includes(item.id)
);
} else {
targetElements = libraryItems.filter((item) => item.id === id2);
}
return targetElements.map((item) => {
return {
...item,
// duplicate each library item before inserting on canvas to confine
// ids and bindings to each library item. See #6465
elements: duplicateElements(item.elements, { randomizeSeed: true })
};
});
},
[libraryItems, selectedItems]
);
const onItemDrag = useCallback9(
(id2, event) => {
event.dataTransfer.setData(
MIME_TYPES.excalidrawlib,
serializeLibraryAsJSON(getInsertedElements(id2))
);
},
[getInsertedElements]
);
const isItemSelected = useCallback9(
(id2) => {
if (!id2) {
return false;
}
return selectedItems.includes(id2);
},
[selectedItems]
);
const onAddToLibraryClick = useCallback9(() => {
onAddToLibrary(pendingElements);
}, [pendingElements, onAddToLibrary]);
const onItemClick = useCallback9(
(id2) => {
if (id2) {
onInsertLibraryItems(getInsertedElements(id2));
}
},
[getInsertedElements, onInsertLibraryItems]
);
const itemsRenderedPerBatch = svgCache.size >= libraryItems.length ? CACHED_ITEMS_RENDERED_PER_BATCH : ITEMS_RENDERED_PER_BATCH;
return /* @__PURE__ */ jsxs35(
"div",
{
className: "library-menu-items-container",
style: pendingElements.length || unpublishedItems.length || publishedItems.length ? { justifyContent: "flex-start" } : { borderBottom: 0 },
children: [
!isLibraryEmpty && /* @__PURE__ */ jsx65(
LibraryDropdownMenu,
{
selectedItems,
onSelectItems,
className: "library-menu-dropdown-container--in-heading"
}
),
/* @__PURE__ */ jsxs35(
Stack_default.Col,
{
className: "library-menu-items-container__items",
align: "start",
gap: 1,
style: {
flex: publishedItems.length > 0 ? 1 : "0 1 auto",
marginBottom: 0
},
ref: libraryContainerRef,
children: [
/* @__PURE__ */ jsxs35(Fragment8, { children: [
!isLibraryEmpty && /* @__PURE__ */ jsx65("div", { className: "library-menu-items-container__header", children: t("labels.personalLib") }),
isLoading && /* @__PURE__ */ jsx65(
"div",
{
style: {
position: "absolute",
top: "var(--container-padding-y)",
right: "var(--container-padding-x)",
transform: "translateY(50%)"
},
children: /* @__PURE__ */ jsx65(Spinner_default, {})
}
),
!pendingElements.length && !unpublishedItems.length ? /* @__PURE__ */ jsxs35("div", { className: "library-menu-items__no-items", children: [
/* @__PURE__ */ jsx65("div", { className: "library-menu-items__no-items__label", children: t("library.noItems") }),
/* @__PURE__ */ jsx65("div", { className: "library-menu-items__no-items__hint", children: publishedItems.length > 0 ? t("library.hint_emptyPrivateLibrary") : t("library.hint_emptyLibrary") })
] }) : /* @__PURE__ */ jsxs35(LibraryMenuSectionGrid, { children: [
pendingElements.length > 0 && /* @__PURE__ */ jsx65(
LibraryMenuSection,
{
itemsRenderedPerBatch,
items: [{ id: null, elements: pendingElements }],
onItemSelectToggle,
onItemDrag,
onClick: onAddToLibraryClick,
isItemSelected,
svgCache
}
),
/* @__PURE__ */ jsx65(
LibraryMenuSection,
{
itemsRenderedPerBatch,
items: unpublishedItems,
onItemSelectToggle,
onItemDrag,
onClick: onItemClick,
isItemSelected,
svgCache
}
)
] })
] }),
/* @__PURE__ */ jsxs35(Fragment8, { children: [
(publishedItems.length > 0 || pendingElements.length > 0 || unpublishedItems.length > 0) && /* @__PURE__ */ jsx65("div", { className: "library-menu-items-container__header library-menu-items-container__header--excal", children: t("labels.excalidrawLib") }),
publishedItems.length > 0 ? /* @__PURE__ */ jsx65(LibraryMenuSectionGrid, { children: /* @__PURE__ */ jsx65(
LibraryMenuSection,
{
itemsRenderedPerBatch,
items: publishedItems,
onItemSelectToggle,
onItemDrag,
onClick: onItemClick,
isItemSelected,
svgCache
}
) }) : unpublishedItems.length > 0 ? /* @__PURE__ */ jsx65(
"div",
{
style: {
margin: "1rem 0",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
fontSize: ".9rem"
},
children: t("library.noItems")
}
) : null
] }),
showBtn && /* @__PURE__ */ jsx65(
LibraryMenuControlButtons,
{
style: { padding: "16px 0", width: "100%" },
id,
libraryReturnUrl,
theme,
children: /* @__PURE__ */ jsx65(
LibraryDropdownMenu,
{
selectedItems,
onSelectItems
}
)
}
)
]
}
)
]
}
);
}
// components/LibraryMenu.tsx
import { jsx as jsx66, jsxs as jsxs36 } from "react/jsx-runtime";
var isLibraryMenuOpenAtom = atom(false);
var LibraryMenuWrapper = ({ children }) => {
return /* @__PURE__ */ jsx66("div", { className: "layer-ui__library", children });
};
var LibraryMenuContent = memo3(
({
onInsertLibraryItems,
pendingElements,
onAddToLibrary,
setAppState,
libraryReturnUrl,
library,
id,
theme,
selectedItems,
onSelectItems
}) => {
const [libraryItemsData] = useAtom(libraryItemsAtom);
const _onAddToLibrary = useCallback10(
(elements) => {
const addToLibrary = async (processedElements, libraryItems2) => {
trackEvent("element", "addToLibrary", "ui");
for (const type of LIBRARY_DISABLED_TYPES) {
if (processedElements.some((element) => element.type === type)) {
return setAppState({
errorMessage: t(`errors.libraryElementTypeError.${type}`)
});
}
}
const nextItems = [
{
status: "unpublished",
elements: processedElements,
id: randomId(),
created: Date.now()
},
...libraryItems2
];
onAddToLibrary();
library.setLibrary(nextItems).catch(() => {
setAppState({ errorMessage: t("alerts.errorAddingToLibrary") });
});
};
addToLibrary(elements, libraryItemsData.libraryItems);
},
[onAddToLibrary, library, setAppState, libraryItemsData.libraryItems]
);
const libraryItems = useMemo6(
() => libraryItemsData.libraryItems,
[libraryItemsData]
);
if (libraryItemsData.status === "loading" && !libraryItemsData.isInitialized) {
return /* @__PURE__ */ jsx66(LibraryMenuWrapper, { children: /* @__PURE__ */ jsx66("div", { className: "layer-ui__library-message", children: /* @__PURE__ */ jsxs36("div", { children: [
/* @__PURE__ */ jsx66(Spinner_default, { size: "2em" }),
/* @__PURE__ */ jsx66("span", { children: t("labels.libraryLoadingMessage") })
] }) }) });
}
const showBtn = libraryItemsData.libraryItems.length > 0 || pendingElements.length > 0;
return /* @__PURE__ */ jsxs36(LibraryMenuWrapper, { children: [
/* @__PURE__ */ jsx66(
LibraryMenuItems,
{
isLoading: libraryItemsData.status === "loading",
libraryItems,
onAddToLibrary: _onAddToLibrary,
onInsertLibraryItems,
pendingElements,
id,
libraryReturnUrl,
theme,
onSelectItems,
selectedItems
}
),
showBtn && /* @__PURE__ */ jsx66(
LibraryMenuControlButtons,
{
className: "library-menu-control-buttons--at-bottom",
style: { padding: "16px 12px 0 12px" },
id,
libraryReturnUrl,
theme
}
)
] });
}
);
var getPendingElements = (elements, selectedElementIds) => ({
elements,
pending: getSelectedElements(
elements,
{ selectedElementIds },
{
includeBoundTextElement: true,
includeElementsInFrames: true
}
),
selectedElementIds
});
var usePendingElementsMemo = (appState, app) => {
const elements = useExcalidrawElements();
const [state, setState] = useState19(
() => getPendingElements(elements, appState.selectedElementIds)
);
const selectedElementVersions = useRef19(
/* @__PURE__ */ new Map()
);
useEffect26(() => {
for (const element of state.pending) {
selectedElementVersions.current.set(element.id, element.version);
}
}, [state.pending]);
useEffect26(() => {
if (
// Only update once pointer is released.
// Reading directly from app.state to make it clear it's not reactive
// (hence, there's potential for stale state)
app.state.cursorButton === "up" && app.state.activeTool.type === "selection"
) {
setState((prev) => {
if (!isShallowEqual(prev.selectedElementIds, appState.selectedElementIds)) {
selectedElementVersions.current.clear();
return getPendingElements(elements, appState.selectedElementIds);
}
const elementsMap = app.scene.getNonDeletedElementsMap();
for (const id of Object.keys(appState.selectedElementIds)) {
const currVersion = elementsMap.get(id)?.version;
if (currVersion && currVersion !== selectedElementVersions.current.get(id)) {
return getPendingElements(elements, appState.selectedElementIds);
}
}
return prev;
});
}
}, [
app,
app.state.cursorButton,
app.state.activeTool.type,
appState.selectedElementIds,
elements
]);
return state.pending;
};
var LibraryMenu = memo3(() => {
const app = useApp();
const { onInsertElements } = app;
const appProps = useAppProps();
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const [selectedItems, setSelectedItems] = useState19([]);
const memoizedLibrary = useMemo6(() => app.library, [app.library]);
const pendingElements = usePendingElementsMemo(appState, app);
const onInsertLibraryItems = useCallback10(
(libraryItems) => {
onInsertElements(distributeLibraryItemsOnSquareGrid(libraryItems));
},
[onInsertElements]
);
const deselectItems = useCallback10(() => {
setAppState({
selectedElementIds: {},
selectedGroupIds: {},
activeEmbeddable: null
});
}, [setAppState]);
return /* @__PURE__ */ jsx66(
LibraryMenuContent,
{
pendingElements,
onInsertLibraryItems,
onAddToLibrary: deselectItems,
setAppState,
libraryReturnUrl: appProps.libraryReturnUrl,
library: memoizedLibrary,
id: app.id,
theme: appState.theme,
selectedItems,
onSelectItems: setSelectedItems
}
);
});
// components/Dialog.tsx
import { jsx as jsx67, jsxs as jsxs37 } from "react/jsx-runtime";
function getDialogSize(size) {
if (size && typeof size === "number") {
return size;
}
switch (size) {
case "small":
return 550;
case "wide":
return 1024;
case "regular":
default:
return 800;
}
}
var Dialog = (props) => {
const [islandNode, setIslandNode] = useCallbackRefState();
const [lastActiveElement] = useState20(document.activeElement);
const { id } = useExcalidrawContainer();
const isFullscreen = useDevice().viewport.isMobile;
useEffect27(() => {
if (!islandNode) {
return;
}
const focusableElements = queryFocusableElements(islandNode);
setTimeout(() => {
if (focusableElements.length > 0 && props.autofocus !== false) {
(focusableElements[1] || focusableElements[0]).focus();
}
});
const handleKeyDown = (event) => {
if (event.key === KEYS.TAB) {
const focusableElements2 = queryFocusableElements(islandNode);
const { activeElement } = document;
const currentIndex = focusableElements2.findIndex(
(element) => element === activeElement
);
if (currentIndex === 0 && event.shiftKey) {
focusableElements2[focusableElements2.length - 1].focus();
event.preventDefault();
} else if (currentIndex === focusableElements2.length - 1 && !event.shiftKey) {
focusableElements2[0].focus();
event.preventDefault();
}
}
};
islandNode.addEventListener("keydown", handleKeyDown);
return () => islandNode.removeEventListener("keydown", handleKeyDown);
}, [islandNode, props.autofocus]);
const setAppState = useExcalidrawSetAppState();
const setIsLibraryMenuOpen = useSetAtom(isLibraryMenuOpenAtom);
const onClose = () => {
setAppState({ openMenu: null });
setIsLibraryMenuOpen(false);
lastActiveElement.focus();
props.onCloseRequest();
};
return /* @__PURE__ */ jsx67(
Modal,
{
className: clsx28("Dialog", props.className, {
"Dialog--fullscreen": isFullscreen
}),
labelledBy: "dialog-title",
maxWidth: getDialogSize(props.size),
onCloseRequest: onClose,
closeOnClickOutside: props.closeOnClickOutside,
children: /* @__PURE__ */ jsxs37(Island, { ref: setIslandNode, children: [
props.title && /* @__PURE__ */ jsx67("h2", { id: `${id}-dialog-title`, className: "Dialog__title", children: /* @__PURE__ */ jsx67("span", { className: "Dialog__titleContent", children: props.title }) }),
isFullscreen && /* @__PURE__ */ jsx67(
"button",
{
className: "Dialog__close",
onClick: onClose,
title: t("buttons.close"),
"aria-label": t("buttons.close"),
type: "button",
children: CloseIcon
}
),
/* @__PURE__ */ jsx67("div", { className: "Dialog__content", children: props.children })
] })
}
);
};
// components/TextField.tsx
import {
forwardRef as forwardRef3,
useRef as useRef20,
useImperativeHandle,
useLayoutEffect as useLayoutEffect3,
useState as useState21
} from "react";
import clsx30 from "clsx";
// components/Button.tsx
import clsx29 from "clsx";
import { jsx as jsx68 } from "react/jsx-runtime";
var Button = ({
type = "button",
onSelect,
selected,
children,
className = "",
...rest
}) => {
return /* @__PURE__ */ jsx68(
"button",
{
onClick: composeEventHandlers(rest.onClick, (event) => {
onSelect();
}),
type,
className: clsx29("excalidraw-button", className, { selected }),
...rest,
children
}
);
};
// components/TextField.tsx
import { jsx as jsx69, jsxs as jsxs38 } from "react/jsx-runtime";
var TextField = forwardRef3(
({
onChange,
label,
fullWidth,
placeholder,
readonly,
selectOnRender,
onKeyDown,
isRedacted = false,
icon,
className,
...rest
}, ref) => {
const innerRef = useRef20(null);
useImperativeHandle(ref, () => innerRef.current);
useLayoutEffect3(() => {
if (selectOnRender) {
innerRef.current?.focus();
innerRef.current?.select();
}
}, [selectOnRender]);
const [isTemporarilyUnredacted, setIsTemporarilyUnredacted] = useState21(false);
return /* @__PURE__ */ jsxs38(
"div",
{
className: clsx30("ExcTextField", className, {
"ExcTextField--fullWidth": fullWidth,
"ExcTextField--hasIcon": !!icon
}),
onClick: () => {
innerRef.current?.focus();
},
children: [
icon,
label && /* @__PURE__ */ jsx69("div", { className: "ExcTextField__label", children: label }),
/* @__PURE__ */ jsxs38(
"div",
{
className: clsx30("ExcTextField__input", {
"ExcTextField__input--readonly": readonly
}),
children: [
/* @__PURE__ */ jsx69(
"input",
{
className: clsx30({
"is-redacted": "value" in rest && rest.value && isRedacted && !isTemporarilyUnredacted
}),
readOnly: readonly,
value: "value" in rest ? rest.value : void 0,
defaultValue: "defaultValue" in rest ? rest.defaultValue : void 0,
placeholder,
ref: innerRef,
onChange: (event) => onChange?.(event.target.value),
onKeyDown
}
),
isRedacted && /* @__PURE__ */ jsx69(
Button,
{
onSelect: () => setIsTemporarilyUnredacted(!isTemporarilyUnredacted),
style: { border: 0, userSelect: "none" },
children: isTemporarilyUnredacted ? eyeClosedIcon : eyeIcon
}
)
]
}
)
]
}
);
}
);
// components/CommandPalette/CommandPalette.tsx
import clsx32 from "clsx";
// actions/shortcuts.ts
var shortcutMap = {
toggleTheme: [getShortcutKey("Shift+Alt+D")],
saveScene: [getShortcutKey("CtrlOrCmd+S")],
loadScene: [getShortcutKey("CtrlOrCmd+O")],
clearCanvas: [getShortcutKey("CtrlOrCmd+Delete")],
imageExport: [getShortcutKey("CtrlOrCmd+Shift+E")],
commandPalette: [
getShortcutKey("CtrlOrCmd+/"),
getShortcutKey("CtrlOrCmd+Shift+P")
],
cut: [getShortcutKey("CtrlOrCmd+X")],
copy: [getShortcutKey("CtrlOrCmd+C")],
paste: [getShortcutKey("CtrlOrCmd+V")],
copyStyles: [getShortcutKey("CtrlOrCmd+Alt+C")],
pasteStyles: [getShortcutKey("CtrlOrCmd+Alt+V")],
selectAll: [getShortcutKey("CtrlOrCmd+A")],
deleteSelectedElements: [getShortcutKey("Delete")],
duplicateSelection: [
getShortcutKey("CtrlOrCmd+D"),
getShortcutKey(`Alt+${t("helpDialog.drag")}`)
],
sendBackward: [getShortcutKey("CtrlOrCmd+[")],
bringForward: [getShortcutKey("CtrlOrCmd+]")],
sendToBack: [
isDarwin ? getShortcutKey("CtrlOrCmd+Alt+[") : getShortcutKey("CtrlOrCmd+Shift+[")
],
bringToFront: [
isDarwin ? getShortcutKey("CtrlOrCmd+Alt+]") : getShortcutKey("CtrlOrCmd+Shift+]")
],
copyAsPng: [getShortcutKey("Shift+Alt+C")],
group: [getShortcutKey("CtrlOrCmd+G")],
ungroup: [getShortcutKey("CtrlOrCmd+Shift+G")],
gridMode: [getShortcutKey("CtrlOrCmd+'")],
zenMode: [getShortcutKey("Alt+Z")],
objectsSnapMode: [getShortcutKey("Alt+S")],
stats: [getShortcutKey("Alt+/")],
addToLibrary: [],
flipHorizontal: [getShortcutKey("Shift+H")],
flipVertical: [getShortcutKey("Shift+V")],
viewMode: [getShortcutKey("Alt+R")],
hyperlink: [getShortcutKey("CtrlOrCmd+K")],
toggleElementLock: [getShortcutKey("CtrlOrCmd+Shift+L")],
resetZoom: [getShortcutKey("CtrlOrCmd+0")],
zoomOut: [getShortcutKey("CtrlOrCmd+-")],
zoomIn: [getShortcutKey("CtrlOrCmd++")],
zoomToFitSelection: [getShortcutKey("Shift+3")],
zoomToFit: [getShortcutKey("Shift+1")],
zoomToFitSelectionInViewport: [getShortcutKey("Shift+2")],
toggleEraserTool: [getShortcutKey("E")],
toggleHandTool: [getShortcutKey("H")],
setFrameAsActiveTool: [getShortcutKey("F")],
saveFileToDisk: [getShortcutKey("CtrlOrCmd+S")],
saveToActiveFile: [getShortcutKey("CtrlOrCmd+S")],
toggleShortcuts: [getShortcutKey("?")],
searchMenu: [getShortcutKey("CtrlOrCmd+F")],
wrapSelectionInFrame: []
};
var getShortcutFromShortcutName = (name, idx = 0) => {
const shortcuts = shortcutMap[name];
return shortcuts && shortcuts.length > 0 ? shortcuts[idx] || shortcuts[0] : "";
};
// components/CommandPalette/CommandPalette.tsx
import fuzzy from "fuzzy";
// deburr.ts
var rsComboMarksRange = "\\u0300-\\u036f";
var reComboHalfMarksRange = "\\ufe20-\\ufe2f";
var rsComboSymbolsRange = "\\u20d0-\\u20ff";
var rsComboRange = rsComboMarksRange + reComboHalfMarksRange + rsComboSymbolsRange;
var rsCombo = `[${rsComboRange}]`;
var reComboMark = RegExp(rsCombo, "g");
var reLatin = /[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g;
var deburredLetters = {
"\xC0": "A",
"\xC1": "A",
"\xC2": "A",
"\xC3": "A",
"\xC4": "A",
"\xC5": "A",
"\xE0": "a",
"\xE1": "a",
"\xE2": "a",
"\xE3": "a",
"\xE4": "a",
"\xE5": "a",
"\xC7": "C",
"\xE7": "c",
"\xD0": "D",
"\xF0": "d",
"\xC8": "E",
"\xC9": "E",
"\xCA": "E",
"\xCB": "E",
"\xE8": "e",
"\xE9": "e",
"\xEA": "e",
"\xEB": "e",
"\xCC": "I",
"\xCD": "I",
"\xCE": "I",
"\xCF": "I",
"\xEC": "i",
"\xED": "i",
"\xEE": "i",
"\xEF": "i",
"\xD1": "N",
"\xF1": "n",
"\xD2": "O",
"\xD3": "O",
"\xD4": "O",
"\xD5": "O",
"\xD6": "O",
"\xD8": "O",
"\xF2": "o",
"\xF3": "o",
"\xF4": "o",
"\xF5": "o",
"\xF6": "o",
"\xF8": "o",
"\xD9": "U",
"\xDA": "U",
"\xDB": "U",
"\xDC": "U",
"\xF9": "u",
"\xFA": "u",
"\xFB": "u",
"\xFC": "u",
"\xDD": "Y",
"\xFD": "y",
"\xFF": "y",
// normaly Ae/ae
"\xC6": "E",
"\xE6": "e",
// normally Th/th
"\xDE": "T",
"\xFE": "t",
// normally ss
"\xDF": "s",
"\u0100": "A",
"\u0102": "A",
"\u0104": "A",
"\u0101": "a",
"\u0103": "a",
"\u0105": "a",
"\u0106": "C",
"\u0108": "C",
"\u010A": "C",
"\u010C": "C",
"\u0107": "c",
"\u0109": "c",
"\u010B": "c",
"\u010D": "c",
"\u010E": "D",
"\u0110": "D",
"\u010F": "d",
"\u0111": "d",
"\u0112": "E",
"\u0114": "E",
"\u0116": "E",
"\u0118": "E",
"\u011A": "E",
"\u0113": "e",
"\u0115": "e",
"\u0117": "e",
"\u0119": "e",
"\u011B": "e",
"\u011C": "G",
"\u011E": "G",
"\u0120": "G",
"\u0122": "G",
"\u011D": "g",
"\u011F": "g",
"\u0121": "g",
"\u0123": "g",
"\u0124": "H",
"\u0126": "H",
"\u0125": "h",
"\u0127": "h",
"\u0128": "I",
"\u012A": "I",
"\u012C": "I",
"\u012E": "I",
"\u0130": "I",
"\u0129": "i",
"\u012B": "i",
"\u012D": "i",
"\u012F": "i",
"\u0131": "i",
"\u0134": "J",
"\u0135": "j",
"\u0136": "K",
"\u0137": "k",
"\u0138": "k",
"\u0139": "L",
"\u013B": "L",
"\u013D": "L",
"\u013F": "L",
"\u0141": "L",
"\u013A": "l",
"\u013C": "l",
"\u013E": "l",
"\u0140": "l",
"\u0142": "l",
"\u0143": "N",
"\u0145": "N",
"\u0147": "N",
"\u014A": "N",
"\u0144": "n",
"\u0146": "n",
"\u0148": "n",
"\u014B": "n",
"\u014C": "O",
"\u014E": "O",
"\u0150": "O",
"\u014D": "o",
"\u014F": "o",
"\u0151": "o",
"\u0154": "R",
"\u0156": "R",
"\u0158": "R",
"\u0155": "r",
"\u0157": "r",
"\u0159": "r",
"\u015A": "S",
"\u015C": "S",
"\u015E": "S",
"\u0160": "S",
"\u015B": "s",
"\u015D": "s",
"\u015F": "s",
"\u0161": "s",
"\u0162": "T",
"\u0164": "T",
"\u0166": "T",
"\u0163": "t",
"\u0165": "t",
"\u0167": "t",
"\u0168": "U",
"\u016A": "U",
"\u016C": "U",
"\u016E": "U",
"\u0170": "U",
"\u0172": "U",
"\u0169": "u",
"\u016B": "u",
"\u016D": "u",
"\u016F": "u",
"\u0171": "u",
"\u0173": "u",
"\u0174": "W",
"\u0175": "w",
"\u0176": "Y",
"\u0177": "y",
"\u0178": "Y",
"\u0179": "Z",
"\u017B": "Z",
"\u017D": "Z",
"\u017A": "z",
"\u017C": "z",
"\u017E": "z",
// normally IJ/ij
"\u0132": "I",
"\u0133": "i",
// normally OE/oe
"\u0152": "E",
"\u0153": "e",
// normally "'n"
"\u0149": "n",
"\u017F": "s"
};
var deburr = (str) => {
return str.replace(reLatin, (key) => {
return deburredLetters[key] || key;
}).replace(reComboMark, "");
};
// components/Actions.tsx
import { useState as useState22 } from "react";
import clsx31 from "clsx";
// context/tunnels.ts
import React27 from "react";
import tunnel from "tunnel-rat";
import { createIsolation as createIsolation2 } from "jotai-scope";
var TunnelsContext = React27.createContext(null);
var useTunnels = () => React27.useContext(TunnelsContext);
var tunnelsJotai = createIsolation2();
var useInitializeTunnels = () => {
return React27.useMemo(() => {
return {
MainMenuTunnel: tunnel(),
WelcomeScreenMenuHintTunnel: tunnel(),
WelcomeScreenToolbarHintTunnel: tunnel(),
WelcomeScreenHelpHintTunnel: tunnel(),
WelcomeScreenCenterTunnel: tunnel(),
FooterCenterTunnel: tunnel(),
DefaultSidebarTriggerTunnel: tunnel(),
DefaultSidebarTabTriggersTunnel: tunnel(),
OverwriteConfirmDialogTunnel: tunnel(),
TTDDialogTriggerTunnel: tunnel(),
tunnelsJotai
};
}, []);
};
// components/Actions.tsx
import { Fragment as Fragment9, jsx as jsx70, jsxs as jsxs39 } from "react/jsx-runtime";
var canChangeStrokeColor = (appState, targetElements) => {
let commonSelectedType = targetElements[0]?.type || null;
for (const element of targetElements) {
if (element.type !== commonSelectedType) {
commonSelectedType = null;
break;
}
}
return hasStrokeColor(appState.activeTool.type) && appState.activeTool.type !== "image" && commonSelectedType !== "image" && commonSelectedType !== "frame" && commonSelectedType !== "magicframe" || targetElements.some((element) => hasStrokeColor(element.type));
};
var canChangeBackgroundColor = (appState, targetElements) => {
return hasBackground(appState.activeTool.type) || targetElements.some((element) => hasBackground(element.type));
};
var SelectedShapeActions = ({
appState,
elementsMap,
renderAction,
app
}) => {
const targetElements = getTargetElements(elementsMap, appState);
let isSingleElementBoundContainer = false;
if (targetElements.length === 2 && (hasBoundTextElement(targetElements[0]) || hasBoundTextElement(targetElements[1]))) {
isSingleElementBoundContainer = true;
}
const isEditingTextOrNewElement = Boolean(
appState.editingTextElement || appState.newElement
);
const device = useDevice();
const isRTL = document.documentElement.getAttribute("dir") === "rtl";
const showFillIcons = hasBackground(appState.activeTool.type) && !isTransparent(appState.currentItemBackgroundColor) || targetElements.some(
(element) => hasBackground(element.type) && !isTransparent(element.backgroundColor)
);
const showLinkIcon = targetElements.length === 1 || isSingleElementBoundContainer;
const showLineEditorAction = !appState.editingLinearElement && targetElements.length === 1 && isLinearElement(targetElements[0]) && !isElbowArrow(targetElements[0]);
const showCropEditorAction = !appState.croppingElementId && targetElements.length === 1 && isImageElement(targetElements[0]);
const showAlignActions = !isSingleElementBoundContainer && alignActionsPredicate(appState, app);
return /* @__PURE__ */ jsxs39("div", { className: "panelColumn", children: [
/* @__PURE__ */ jsx70("div", { children: canChangeStrokeColor(appState, targetElements) && renderAction("changeStrokeColor") }),
canChangeBackgroundColor(appState, targetElements) && /* @__PURE__ */ jsx70("div", { children: renderAction("changeBackgroundColor") }),
showFillIcons && renderAction("changeFillStyle"),
(hasStrokeWidth(appState.activeTool.type) || targetElements.some((element) => hasStrokeWidth(element.type))) && renderAction("changeStrokeWidth"),
(appState.activeTool.type === "freedraw" || targetElements.some((element) => element.type === "freedraw")) && renderAction("changeStrokeShape"),
(hasStrokeStyle(appState.activeTool.type) || targetElements.some((element) => hasStrokeStyle(element.type))) && /* @__PURE__ */ jsxs39(Fragment9, { children: [
renderAction("changeStrokeStyle"),
renderAction("changeSloppiness")
] }),
(canChangeRoundness(appState.activeTool.type) || targetElements.some((element) => canChangeRoundness(element.type))) && /* @__PURE__ */ jsx70(Fragment9, { children: renderAction("changeRoundness") }),
(toolIsArrow(appState.activeTool.type) || targetElements.some((element) => toolIsArrow(element.type))) && /* @__PURE__ */ jsx70(Fragment9, { children: renderAction("changeArrowType") }),
(appState.activeTool.type === "text" || targetElements.some(isTextElement)) && /* @__PURE__ */ jsxs39(Fragment9, { children: [
renderAction("changeFontFamily"),
renderAction("changeFontSize"),
(appState.activeTool.type === "text" || suppportsHorizontalAlign(targetElements, elementsMap)) && renderAction("changeTextAlign")
] }),
shouldAllowVerticalAlign(targetElements, elementsMap) && renderAction("changeVerticalAlign"),
(canHaveArrowheads(appState.activeTool.type) || targetElements.some((element) => canHaveArrowheads(element.type))) && /* @__PURE__ */ jsx70(Fragment9, { children: renderAction("changeArrowhead") }),
renderAction("changeOpacity"),
/* @__PURE__ */ jsxs39("fieldset", { children: [
/* @__PURE__ */ jsx70("legend", { children: t("labels.layers") }),
/* @__PURE__ */ jsxs39("div", { className: "buttonList", children: [
renderAction("sendToBack"),
renderAction("sendBackward"),
renderAction("bringForward"),
renderAction("bringToFront")
] })
] }),
showAlignActions && !isSingleElementBoundContainer && /* @__PURE__ */ jsxs39("fieldset", { children: [
/* @__PURE__ */ jsx70("legend", { children: t("labels.align") }),
/* @__PURE__ */ jsxs39("div", { className: "buttonList", children: [
isRTL ? /* @__PURE__ */ jsxs39(Fragment9, { children: [
renderAction("alignRight"),
renderAction("alignHorizontallyCentered"),
renderAction("alignLeft")
] }) : /* @__PURE__ */ jsxs39(Fragment9, { children: [
renderAction("alignLeft"),
renderAction("alignHorizontallyCentered"),
renderAction("alignRight")
] }),
targetElements.length > 2 && renderAction("distributeHorizontally"),
/* @__PURE__ */ jsx70("div", { style: { flexBasis: "100%", height: 0 } }),
/* @__PURE__ */ jsxs39(
"div",
{
style: {
display: "flex",
flexWrap: "wrap",
gap: ".5rem",
marginTop: "-0.5rem"
},
children: [
renderAction("alignTop"),
renderAction("alignVerticallyCentered"),
renderAction("alignBottom"),
targetElements.length > 2 && renderAction("distributeVertically")
]
}
)
] })
] }),
!isEditingTextOrNewElement && targetElements.length > 0 && /* @__PURE__ */ jsxs39("fieldset", { children: [
/* @__PURE__ */ jsx70("legend", { children: t("labels.actions") }),
/* @__PURE__ */ jsxs39("div", { className: "buttonList", children: [
!device.editor.isMobile && renderAction("duplicateSelection"),
!device.editor.isMobile && renderAction("deleteSelectedElements"),
renderAction("group"),
renderAction("ungroup"),
showLinkIcon && renderAction("hyperlink"),
showCropEditorAction && renderAction("cropEditor"),
showLineEditorAction && renderAction("toggleLinearEditor")
] })
] })
] });
};
var ShapesSwitcher = ({
activeTool,
appState,
app,
UIOptions
}) => {
const [isExtraToolsMenuOpen, setIsExtraToolsMenuOpen] = useState22(false);
const frameToolSelected = activeTool.type === "frame";
const laserToolSelected = activeTool.type === "laser";
const embeddableToolSelected = activeTool.type === "embeddable";
const { TTDDialogTriggerTunnel } = useTunnels();
return /* @__PURE__ */ jsxs39(Fragment9, { children: [
SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
if (UIOptions.tools?.[value] === false) {
return null;
}
const label = t(`toolBar.${value}`);
const letter = key && capitalizeString(typeof key === "string" ? key : key[0]);
const shortcut = letter ? `${letter} ${t("helpDialog.or")} ${numericKey}` : `${numericKey}`;
return /* @__PURE__ */ jsx70(
ToolButton,
{
className: clsx31("Shape", { fillable }),
type: "radio",
icon,
checked: activeTool.type === value,
name: "editor-current-shape",
title: `${capitalizeString(label)} \u2014 ${shortcut}`,
keyBindingLabel: numericKey || letter,
"aria-label": capitalizeString(label),
"aria-keyshortcuts": shortcut,
"data-testid": `toolbar-${value}`,
onPointerDown: ({ pointerType }) => {
if (!appState.penDetected && pointerType === "pen") {
app.togglePenMode(true);
}
},
onChange: ({ pointerType }) => {
if (appState.activeTool.type !== value) {
trackEvent("toolbar", value, "ui");
}
if (value === "image") {
app.setActiveTool({
type: value,
insertOnCanvasDirectly: pointerType !== "mouse"
});
} else {
app.setActiveTool({ type: value });
}
}
},
value
);
}),
/* @__PURE__ */ jsx70("div", { className: "App-toolbar__divider" }),
/* @__PURE__ */ jsxs39(DropdownMenu_default, { open: isExtraToolsMenuOpen, children: [
/* @__PURE__ */ jsx70(
DropdownMenu_default.Trigger,
{
className: clsx31("App-toolbar__extra-tools-trigger", {
"App-toolbar__extra-tools-trigger--selected": frameToolSelected || embeddableToolSelected || // in collab we're already highlighting the laser button
// outside toolbar, so let's not highlight extra-tools button
// on top of it
laserToolSelected && !app.props.isCollaborating
}),
onToggle: () => setIsExtraToolsMenuOpen(!isExtraToolsMenuOpen),
title: t("toolBar.extraTools"),
children: extraToolsIcon
}
),
/* @__PURE__ */ jsxs39(
DropdownMenu_default.Content,
{
onClickOutside: () => setIsExtraToolsMenuOpen(false),
onSelect: () => setIsExtraToolsMenuOpen(false),
className: "App-toolbar__extra-tools-dropdown",
children: [
/* @__PURE__ */ jsx70(
DropdownMenu_default.Item,
{
onSelect: () => app.setActiveTool({ type: "frame" }),
icon: frameToolIcon,
shortcut: KEYS.F.toLocaleUpperCase(),
"data-testid": "toolbar-frame",
selected: frameToolSelected,
children: t("toolBar.frame")
}
),
/* @__PURE__ */ jsx70(
DropdownMenu_default.Item,
{
onSelect: () => app.setActiveTool({ type: "embeddable" }),
icon: EmbedIcon,
"data-testid": "toolbar-embeddable",
selected: embeddableToolSelected,
children: t("toolBar.embeddable")
}
),
/* @__PURE__ */ jsx70(
DropdownMenu_default.Item,
{
onSelect: () => app.setActiveTool({ type: "laser" }),
icon: laserPointerToolIcon,
"data-testid": "toolbar-laser",
selected: laserToolSelected,
shortcut: KEYS.K.toLocaleUpperCase(),
children: t("toolBar.laser")
}
),
/* @__PURE__ */ jsx70("div", { style: { margin: "6px 0", fontSize: 14, fontWeight: 600 }, children: "Generate" }),
app.props.aiEnabled !== false && /* @__PURE__ */ jsx70(TTDDialogTriggerTunnel.Out, {}),
/* @__PURE__ */ jsx70(
DropdownMenu_default.Item,
{
onSelect: () => app.setOpenDialog({ name: "ttd", tab: "mermaid" }),
icon: mermaidLogoIcon,
"data-testid": "toolbar-embeddable",
children: t("toolBar.mermaidToExcalidraw")
}
),
app.props.aiEnabled !== false && app.plugins.diagramToCode && /* @__PURE__ */ jsx70(Fragment9, { children: /* @__PURE__ */ jsxs39(
DropdownMenu_default.Item,
{
onSelect: () => app.onMagicframeToolSelect(),
icon: MagicIcon,
"data-testid": "toolbar-magicframe",
children: [
t("toolBar.magicframe"),
/* @__PURE__ */ jsx70(DropdownMenu_default.Item.Badge, { children: "AI" })
]
}
) })
]
}
)
] })
] });
};
var ZoomActions = ({
renderAction,
zoom
}) => /* @__PURE__ */ jsx70(Stack_default.Col, { gap: 1, className: CLASSES.ZOOM_ACTIONS, children: /* @__PURE__ */ jsxs39(Stack_default.Row, { align: "center", children: [
renderAction("zoomOut"),
renderAction("resetZoom"),
renderAction("zoomIn")
] }) });
var UndoRedoActions = ({
renderAction,
className
}) => /* @__PURE__ */ jsxs39("div", { className: `undo-redo-buttons ${className}`, children: [
/* @__PURE__ */ jsx70("div", { className: "undo-button-container", children: /* @__PURE__ */ jsx70(Tooltip, { label: t("buttons.undo"), children: renderAction("undo") }) }),
/* @__PURE__ */ jsx70("div", { className: "redo-button-container", children: /* @__PURE__ */ jsxs39(Tooltip, { label: t("buttons.redo"), children: [
" ",
renderAction("redo")
] }) })
] });
var ExitZenModeAction = ({
actionManager,
showExitZenModeBtn
}) => /* @__PURE__ */ jsx70(
"button",
{
type: "button",
className: clsx31("disable-zen-mode", {
"disable-zen-mode--visible": showExitZenModeBtn
}),
onClick: () => actionManager.executeAction(actionToggleZenMode),
children: t("buttons.exitZenMode")
}
);
var FinalizeAction = ({
renderAction,
className
}) => /* @__PURE__ */ jsx70("div", { className: `finalize-button ${className}`, children: renderAction("finalize", { size: "small" }) });
// hooks/useStableCallback.ts
import { useRef as useRef21 } from "react";
var useStableCallback = (userFn) => {
const stableRef = useRef21({ userFn });
stableRef.current.userFn = userFn;
if (!stableRef.current.stableFn) {
stableRef.current.stableFn = (...args) => stableRef.current.userFn(...args);
}
return stableRef.current.stableFn;
};
// components/ActiveConfirmDialog.tsx
import { jsx as jsx71, jsxs as jsxs40 } from "react/jsx-runtime";
var activeConfirmDialogAtom = atom(null);
var ActiveConfirmDialog = () => {
const [activeConfirmDialog, setActiveConfirmDialog] = useAtom(
activeConfirmDialogAtom
);
const actionManager = useExcalidrawActionManager();
if (!activeConfirmDialog) {
return null;
}
if (activeConfirmDialog === "clearCanvas") {
return /* @__PURE__ */ jsx71(
ConfirmDialog_default,
{
onConfirm: () => {
actionManager.executeAction(actionClearCanvas);
setActiveConfirmDialog(null);
},
onCancel: () => setActiveConfirmDialog(null),
title: t("clearCanvasDialog.title"),
children: /* @__PURE__ */ jsxs40("p", { className: "clear-canvas__content", children: [
" ",
t("alerts.clearReset")
] })
}
);
}
return null;
};
// components/CommandPalette/defaultCommandPaletteItems.ts
var defaultCommandPaletteItems_exports = {};
__export(defaultCommandPaletteItems_exports, {
toggleTheme: () => toggleTheme
});
var toggleTheme = {
...actionToggleTheme,
category: "App",
label: "Toggle theme",
perform: ({ actionManager }) => {
actionManager.executeAction(actionToggleTheme, "commandPalette");
}
};
// actions/actionElementLink.ts
var actionCopyElementLink = register({
name: "copyElementLink",
label: "labels.copyElementLink",
icon: copyIcon,
trackEvent: { category: "element" },
perform: async (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState);
try {
if (window.location) {
const idAndType = getLinkIdAndTypeFromSelection(
selectedElements,
appState
);
if (idAndType) {
await copyTextToSystemClipboard(
app.props.generateLinkForSelection ? app.props.generateLinkForSelection(idAndType.id, idAndType.type) : defaultGetElementLinkFromSelection(
idAndType.id,
idAndType.type
)
);
return {
appState: {
toast: {
message: t("toast.elementLinkCopied"),
closable: true
}
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
return {
appState,
elements,
app,
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
} catch (error) {
console.error(error);
}
return {
appState,
elements,
app,
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
predicate: (elements, appState) => canCreateLinkFromElements(getSelectedElements(elements, appState))
});
var actionLinkToElement = register({
name: "linkToElement",
label: "labels.linkToElement",
icon: elementLinkIcon,
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState);
if (selectedElements.length !== 1 || !canCreateLinkFromElements(selectedElements)) {
return {
elements,
appState,
app,
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
return {
appState: {
...appState,
openDialog: {
name: "elementLinkSelector",
sourceElementId: getSelectedElements(elements, appState)[0].id
}
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
predicate: (elements, appState, appProps, app) => {
const selectedElements = getSelectedElements(elements, appState);
return appState.openDialog?.name !== "elementLinkSelector" && selectedElements.length === 1 && canCreateLinkFromElements(selectedElements);
},
trackEvent: false
});
// components/CommandPalette/CommandPalette.tsx
import { jsx as jsx72, jsxs as jsxs41 } from "react/jsx-runtime";
var lastUsedPaletteItem = atom(null);
var DEFAULT_CATEGORIES = {
app: "App",
export: "Export",
tools: "Tools",
editor: "Editor",
elements: "Elements",
links: "Links"
};
var getCategoryOrder = (category) => {
switch (category) {
case DEFAULT_CATEGORIES.app:
return 1;
case DEFAULT_CATEGORIES.export:
return 2;
case DEFAULT_CATEGORIES.editor:
return 3;
case DEFAULT_CATEGORIES.tools:
return 4;
case DEFAULT_CATEGORIES.elements:
return 5;
case DEFAULT_CATEGORIES.links:
return 6;
default:
return 10;
}
};
var CommandShortcutHint = ({
shortcut,
className,
children
}) => {
const shortcuts = shortcut.replace("++", "+$").split("+");
return /* @__PURE__ */ jsxs41("div", { className: clsx32("shortcut", className), children: [
shortcuts.map((item, idx) => {
return /* @__PURE__ */ jsx72("div", { className: "shortcut-wrapper", children: /* @__PURE__ */ jsx72("div", { className: "shortcut-key", children: item === "$" ? "+" : item }) }, item);
}),
/* @__PURE__ */ jsx72("div", { className: "shortcut-desc", children })
] });
};
var isCommandPaletteToggleShortcut = (event) => {
return !event.altKey && event[KEYS.CTRL_OR_CMD] && (event.shiftKey && event.key.toLowerCase() === KEYS.P || event.key === KEYS.SLASH);
};
var CommandPalette = Object.assign(
(props) => {
const uiAppState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
useEffect28(() => {
const commandPaletteShortcut = (event) => {
if (isCommandPaletteToggleShortcut(event)) {
event.preventDefault();
event.stopPropagation();
setAppState((appState) => {
const nextState = appState.openDialog?.name === "commandPalette" ? null : { name: "commandPalette" };
if (nextState) {
trackEvent("command_palette", "open", "shortcut");
}
return {
openDialog: nextState
};
});
}
};
window.addEventListener("keydown" /* KEYDOWN */, commandPaletteShortcut, {
capture: true
});
return () => window.removeEventListener("keydown" /* KEYDOWN */, commandPaletteShortcut, {
capture: true
});
}, [setAppState]);
if (uiAppState.openDialog?.name !== "commandPalette") {
return null;
}
return /* @__PURE__ */ jsx72(CommandPaletteInner, { ...props });
},
{
defaultItems: defaultCommandPaletteItems_exports
}
);
function CommandPaletteInner({
customCommandPaletteItems
}) {
const app = useApp();
const uiAppState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const appProps = useAppProps();
const actionManager = useExcalidrawActionManager();
const [lastUsed, setLastUsed] = useAtom(lastUsedPaletteItem);
const [allCommands, setAllCommands] = useState23(null);
const inputRef = useRef22(null);
const stableDeps = useStable({
uiAppState,
customCommandPaletteItems,
appProps
});
useEffect28(() => {
const { uiAppState: uiAppState2, customCommandPaletteItems: customCommandPaletteItems2, appProps: appProps2 } = stableDeps;
const getActionLabel = (action) => {
let label = "";
if (action.label) {
if (typeof action.label === "function") {
label = t(
action.label(
app.scene.getNonDeletedElements(),
uiAppState2,
app
)
);
} else {
label = t(action.label);
}
}
return label;
};
const getActionIcon = (action) => {
if (typeof action.icon === "function") {
return action.icon(uiAppState2, app.scene.getNonDeletedElements());
}
return action.icon;
};
let commandsFromActions = [];
const actionToCommand = (action, category, transformer) => {
const command = {
label: getActionLabel(action),
icon: getActionIcon(action),
category,
shortcut: getShortcutFromShortcutName(action.name),
keywords: action.keywords,
predicate: action.predicate,
viewMode: action.viewMode,
perform: () => {
actionManager.executeAction(action, "commandPalette");
}
};
return transformer ? transformer(command, action) : command;
};
if (uiAppState2 && app.scene && actionManager) {
const elementsCommands = [
actionManager.actions.group,
actionManager.actions.ungroup,
actionManager.actions.cut,
actionManager.actions.copy,
actionManager.actions.deleteSelectedElements,
actionManager.actions.wrapSelectionInFrame,
actionManager.actions.copyStyles,
actionManager.actions.pasteStyles,
actionManager.actions.bringToFront,
actionManager.actions.bringForward,
actionManager.actions.sendBackward,
actionManager.actions.sendToBack,
actionManager.actions.alignTop,
actionManager.actions.alignBottom,
actionManager.actions.alignLeft,
actionManager.actions.alignRight,
actionManager.actions.alignVerticallyCentered,
actionManager.actions.alignHorizontallyCentered,
actionManager.actions.duplicateSelection,
actionManager.actions.flipHorizontal,
actionManager.actions.flipVertical,
actionManager.actions.zoomToFitSelection,
actionManager.actions.zoomToFitSelectionInViewport,
actionManager.actions.increaseFontSize,
actionManager.actions.decreaseFontSize,
actionManager.actions.toggleLinearEditor,
actionManager.actions.cropEditor,
actionLink,
actionCopyElementLink,
actionLinkToElement
].map(
(action) => actionToCommand(
action,
DEFAULT_CATEGORIES.elements,
(command, action2) => ({
...command,
predicate: action2.predicate ? action2.predicate : (elements, appState, appProps3, app2) => {
const selectedElements = getSelectedElements(
elements,
appState
);
return selectedElements.length > 0;
}
})
)
);
const toolCommands = [
actionManager.actions.toggleHandTool,
actionManager.actions.setFrameAsActiveTool
].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.tools));
const editorCommands = [
actionManager.actions.undo,
actionManager.actions.redo,
actionManager.actions.zoomIn,
actionManager.actions.zoomOut,
actionManager.actions.resetZoom,
actionManager.actions.zoomToFit,
actionManager.actions.zenMode,
actionManager.actions.viewMode,
actionManager.actions.gridMode,
actionManager.actions.objectsSnapMode,
actionManager.actions.toggleShortcuts,
actionManager.actions.selectAll,
actionManager.actions.toggleElementLock,
actionManager.actions.unlockAllElements,
actionManager.actions.stats
].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.editor));
const exportCommands = [
actionManager.actions.saveToActiveFile,
actionManager.actions.saveFileToDisk,
actionManager.actions.copyAsPng,
actionManager.actions.copyAsSvg
].map((action) => actionToCommand(action, DEFAULT_CATEGORIES.export));
commandsFromActions = [
...elementsCommands,
...editorCommands,
{
label: getActionLabel(actionClearCanvas),
icon: getActionIcon(actionClearCanvas),
shortcut: getShortcutFromShortcutName(
actionClearCanvas.name
),
category: DEFAULT_CATEGORIES.editor,
keywords: ["delete", "destroy"],
viewMode: false,
perform: () => {
editorJotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
}
},
{
label: t("buttons.exportImage"),
category: DEFAULT_CATEGORIES.export,
icon: ExportImageIcon,
shortcut: getShortcutFromShortcutName("imageExport"),
keywords: [
"export",
"image",
"png",
"jpeg",
"svg",
"clipboard",
"picture"
],
perform: () => {
setAppState({ openDialog: { name: "imageExport" } });
}
},
...exportCommands
];
const additionalCommands = [
{
label: t("toolBar.library"),
category: DEFAULT_CATEGORIES.app,
icon: LibraryIcon,
viewMode: false,
perform: () => {
if (uiAppState2.openSidebar) {
setAppState({
openSidebar: null
});
} else {
setAppState({
openSidebar: {
name: DEFAULT_SIDEBAR.name,
tab: DEFAULT_SIDEBAR.defaultTab
}
});
}
}
},
{
label: t("search.title"),
category: DEFAULT_CATEGORIES.app,
icon: searchIcon,
viewMode: true,
perform: () => {
actionManager.executeAction(actionToggleSearchMenu);
}
},
{
label: t("labels.changeStroke"),
keywords: ["color", "outline"],
category: DEFAULT_CATEGORIES.elements,
icon: bucketFillIcon,
viewMode: false,
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length > 0 && canChangeStrokeColor(appState, selectedElements);
},
perform: () => {
setAppState((prevState) => ({
openMenu: prevState.openMenu === "shape" ? null : "shape",
openPopup: "elementStroke"
}));
}
},
{
label: t("labels.changeBackground"),
keywords: ["color", "fill"],
icon: bucketFillIcon,
category: DEFAULT_CATEGORIES.elements,
viewMode: false,
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length > 0 && canChangeBackgroundColor(appState, selectedElements);
},
perform: () => {
setAppState((prevState) => ({
openMenu: prevState.openMenu === "shape" ? null : "shape",
openPopup: "elementBackground"
}));
}
},
{
label: t("labels.canvasBackground"),
keywords: ["color"],
icon: bucketFillIcon,
category: DEFAULT_CATEGORIES.editor,
viewMode: false,
perform: () => {
setAppState((prevState) => ({
openMenu: prevState.openMenu === "canvas" ? null : "canvas",
openPopup: "canvasBackground"
}));
}
},
...SHAPES.reduce((acc, shape) => {
const { value, icon, key, numericKey } = shape;
if (appProps2.UIOptions.tools?.[value] === false) {
return acc;
}
const letter = key && capitalizeString(typeof key === "string" ? key : key[0]);
const shortcut = letter || numericKey;
const command = {
label: t(`toolBar.${value}`),
category: DEFAULT_CATEGORIES.tools,
shortcut,
icon,
keywords: ["toolbar"],
viewMode: false,
perform: ({ event }) => {
if (value === "image") {
app.setActiveTool({
type: value,
insertOnCanvasDirectly: event.type === "keydown" /* KEYDOWN */
});
} else {
app.setActiveTool({ type: value });
}
}
};
acc.push(command);
return acc;
}, []),
...toolCommands,
{
label: t("toolBar.lock"),
category: DEFAULT_CATEGORIES.tools,
icon: uiAppState2.activeTool.locked ? LockedIcon : UnlockedIcon,
shortcut: KEYS.Q.toLocaleUpperCase(),
viewMode: false,
perform: () => {
app.toggleLock();
}
},
{
label: `${t("labels.textToDiagram")}...`,
category: DEFAULT_CATEGORIES.tools,
icon: brainIconThin,
viewMode: false,
predicate: appProps2.aiEnabled,
perform: () => {
setAppState((state) => ({
...state,
openDialog: {
name: "ttd",
tab: "text-to-diagram"
}
}));
}
},
{
label: `${t("toolBar.mermaidToExcalidraw")}...`,
category: DEFAULT_CATEGORIES.tools,
icon: mermaidLogoIcon,
viewMode: false,
predicate: appProps2.aiEnabled,
perform: () => {
setAppState((state) => ({
...state,
openDialog: {
name: "ttd",
tab: "mermaid"
}
}));
}
}
// {
// label: `${t("toolBar.magicframe")}...`,
// category: DEFAULT_CATEGORIES.tools,
// icon: MagicIconThin,
// viewMode: false,
// predicate: appProps.aiEnabled,
// perform: () => {
// app.onMagicframeToolSelect();
// },
// },
];
const allCommands2 = [
...commandsFromActions,
...additionalCommands,
...customCommandPaletteItems2 || []
].map((command) => {
return {
...command,
icon: command.icon || boltIcon,
order: command.order ?? getCategoryOrder(command.category),
haystack: `${deburr(command.label.toLocaleLowerCase())} ${command.keywords?.join(" ") || ""}`
};
});
setAllCommands(allCommands2);
setLastUsed(
allCommands2.find((command) => command.label === lastUsed?.label) ?? null
);
}
}, [
stableDeps,
app,
actionManager,
setAllCommands,
lastUsed?.label,
setLastUsed,
setAppState
]);
const [commandSearch, setCommandSearch] = useState23("");
const [currentCommand, setCurrentCommand] = useState23(null);
const [commandsByCategory, setCommandsByCategory] = useState23({});
const closeCommandPalette = (cb) => {
setAppState(
{
openDialog: null
},
cb
);
setCommandSearch("");
};
const executeCommand = (command, event) => {
if (uiAppState.openDialog?.name === "commandPalette") {
event.stopPropagation();
event.preventDefault();
document.body.classList.add("excalidraw-animations-disabled");
closeCommandPalette(() => {
command.perform({ actionManager, event });
setLastUsed(command);
requestAnimationFrame(() => {
document.body.classList.remove("excalidraw-animations-disabled");
});
});
}
};
const isCommandAvailable = useStableCallback(
(command) => {
if (command.viewMode === false && uiAppState.viewModeEnabled) {
return false;
}
return typeof command.predicate === "function" ? command.predicate(
app.scene.getNonDeletedElements(),
uiAppState,
appProps,
app
) : command.predicate === void 0 || command.predicate;
}
);
const handleKeyDown = useStableCallback((event) => {
const ignoreAlphanumerics = isWritableElement(event.target) || isCommandPaletteToggleShortcut(event) || event.key === KEYS.ESCAPE;
if (ignoreAlphanumerics && event.key !== KEYS.ARROW_UP && event.key !== KEYS.ARROW_DOWN && event.key !== KEYS.ENTER) {
return;
}
const matchingCommands = Object.values(commandsByCategory).flat();
const shouldConsiderLastUsed = lastUsed && !commandSearch && isCommandAvailable(lastUsed);
if (event.key === KEYS.ARROW_UP) {
event.preventDefault();
const index = matchingCommands.findIndex(
(item) => item.label === currentCommand?.label
);
if (shouldConsiderLastUsed) {
if (index === 0) {
setCurrentCommand(lastUsed);
return;
}
if (currentCommand === lastUsed) {
const nextItem2 = matchingCommands[matchingCommands.length - 1];
if (nextItem2) {
setCurrentCommand(nextItem2);
}
return;
}
}
let nextIndex;
if (index === -1) {
nextIndex = matchingCommands.length - 1;
} else {
nextIndex = index === 0 ? matchingCommands.length - 1 : (index - 1) % matchingCommands.length;
}
const nextItem = matchingCommands[nextIndex];
if (nextItem) {
setCurrentCommand(nextItem);
}
return;
}
if (event.key === KEYS.ARROW_DOWN) {
event.preventDefault();
const index = matchingCommands.findIndex(
(item) => item.label === currentCommand?.label
);
if (shouldConsiderLastUsed) {
if (!currentCommand || index === matchingCommands.length - 1) {
setCurrentCommand(lastUsed);
return;
}
if (currentCommand === lastUsed) {
const nextItem2 = matchingCommands[0];
if (nextItem2) {
setCurrentCommand(nextItem2);
}
return;
}
}
const nextIndex = (index + 1) % matchingCommands.length;
const nextItem = matchingCommands[nextIndex];
if (nextItem) {
setCurrentCommand(nextItem);
}
return;
}
if (event.key === KEYS.ENTER) {
if (currentCommand) {
setTimeout(() => {
executeCommand(currentCommand, event);
});
}
}
if (ignoreAlphanumerics) {
return;
}
event.stopPropagation();
if (/^[a-zA-Z0-9]$/.test(event.key)) {
inputRef?.current?.focus();
return;
}
event.preventDefault();
});
useEffect28(() => {
window.addEventListener("keydown" /* KEYDOWN */, handleKeyDown, {
capture: true
});
return () => window.removeEventListener("keydown" /* KEYDOWN */, handleKeyDown, {
capture: true
});
}, [handleKeyDown]);
useEffect28(() => {
if (!allCommands) {
return;
}
const getNextCommandsByCategory = (commands) => {
const nextCommandsByCategory = {};
for (const command of commands) {
if (nextCommandsByCategory[command.category]) {
nextCommandsByCategory[command.category].push(command);
} else {
nextCommandsByCategory[command.category] = [command];
}
}
return nextCommandsByCategory;
};
let matchingCommands = allCommands.filter(isCommandAvailable).sort((a, b) => a.order - b.order);
const showLastUsed = !commandSearch && lastUsed && isCommandAvailable(lastUsed);
if (!commandSearch) {
setCommandsByCategory(
getNextCommandsByCategory(
showLastUsed ? matchingCommands.filter(
(command) => command.label !== lastUsed?.label
) : matchingCommands
)
);
setCurrentCommand(showLastUsed ? lastUsed : matchingCommands[0] || null);
return;
}
const _query = deburr(
commandSearch.toLocaleLowerCase().replace(/[<>_| -]/g, "")
);
matchingCommands = fuzzy.filter(_query, matchingCommands, {
extract: (command) => command.haystack
}).sort((a, b) => b.score - a.score).map((item) => item.original);
setCommandsByCategory(getNextCommandsByCategory(matchingCommands));
setCurrentCommand(matchingCommands[0] ?? null);
}, [commandSearch, allCommands, isCommandAvailable, lastUsed]);
return /* @__PURE__ */ jsxs41(
Dialog,
{
onCloseRequest: () => closeCommandPalette(),
closeOnClickOutside: true,
title: false,
size: 720,
autofocus: true,
className: "command-palette-dialog",
children: [
/* @__PURE__ */ jsx72(
TextField,
{
value: commandSearch,
placeholder: t("commandPalette.search.placeholder"),
onChange: (value) => {
setCommandSearch(value);
},
selectOnRender: true,
ref: inputRef
}
),
!app.device.viewport.isMobile && /* @__PURE__ */ jsxs41("div", { className: "shortcuts-wrapper", children: [
/* @__PURE__ */ jsx72(CommandShortcutHint, { shortcut: "\u2191\u2193", children: t("commandPalette.shortcuts.select") }),
/* @__PURE__ */ jsx72(CommandShortcutHint, { shortcut: "\u21B5", children: t("commandPalette.shortcuts.confirm") }),
/* @__PURE__ */ jsx72(CommandShortcutHint, { shortcut: getShortcutKey("Esc"), children: t("commandPalette.shortcuts.close") })
] }),
/* @__PURE__ */ jsxs41("div", { className: "commands", children: [
lastUsed && !commandSearch && /* @__PURE__ */ jsxs41("div", { className: "command-category", children: [
/* @__PURE__ */ jsxs41("div", { className: "command-category-title", children: [
t("commandPalette.recents"),
/* @__PURE__ */ jsx72(
"div",
{
className: "icon",
style: {
marginLeft: "6px"
},
children: clockIcon
}
)
] }),
/* @__PURE__ */ jsx72(
CommandItem,
{
command: lastUsed,
isSelected: lastUsed.label === currentCommand?.label,
onClick: (event) => executeCommand(lastUsed, event),
disabled: !isCommandAvailable(lastUsed),
onMouseMove: () => setCurrentCommand(lastUsed),
showShortcut: !app.device.viewport.isMobile,
appState: uiAppState
}
)
] }),
Object.keys(commandsByCategory).length > 0 ? Object.keys(commandsByCategory).map((category, idx) => {
return /* @__PURE__ */ jsxs41("div", { className: "command-category", children: [
/* @__PURE__ */ jsx72("div", { className: "command-category-title", children: category }),
commandsByCategory[category].map((command) => /* @__PURE__ */ jsx72(
CommandItem,
{
command,
isSelected: command.label === currentCommand?.label,
onClick: (event) => executeCommand(command, event),
onMouseMove: () => setCurrentCommand(command),
showShortcut: !app.device.viewport.isMobile,
appState: uiAppState
},
command.label
))
] }, category);
}) : allCommands ? /* @__PURE__ */ jsxs41("div", { className: "no-match", children: [
/* @__PURE__ */ jsx72("div", { className: "icon", children: searchIcon }),
" ",
t("commandPalette.search.noMatch")
] }) : null
] })
]
}
);
}
var CommandItem = ({
command,
isSelected,
disabled,
onMouseMove,
onClick,
showShortcut,
appState
}) => {
const noop = () => {
};
return /* @__PURE__ */ jsxs41(
"div",
{
className: clsx32("command-item", {
"item-selected": isSelected,
"item-disabled": disabled
}),
ref: (ref) => {
if (isSelected && !disabled) {
ref?.scrollIntoView?.({
block: "nearest"
});
}
},
onClick: disabled ? noop : onClick,
onMouseMove: disabled ? noop : onMouseMove,
title: disabled ? t("commandPalette.itemNotAvailable") : "",
children: [
/* @__PURE__ */ jsxs41("div", { className: "name", children: [
command.icon && /* @__PURE__ */ jsx72(
InlineIcon,
{
icon: typeof command.icon === "function" ? command.icon(appState) : command.icon
}
),
command.label
] }),
showShortcut && command.shortcut && /* @__PURE__ */ jsx72(CommandShortcutHint, { shortcut: command.shortcut })
]
}
);
};
// actions/actionLinearEditor.tsx
import { jsx as jsx73 } from "react/jsx-runtime";
var actionToggleLinearEditor = register({
name: "toggleLinearEditor",
category: DEFAULT_CATEGORIES.elements,
label: (elements, appState, app) => {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds
})[0];
return selectedElement?.type === "arrow" ? "labels.lineEditor.editArrow" : "labels.lineEditor.edit";
},
keywords: ["line"],
trackEvent: {
category: "element"
},
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
if (!appState.editingLinearElement && selectedElements.length === 1 && isLinearElement(selectedElements[0]) && !isElbowArrow(selectedElements[0])) {
return true;
}
return false;
},
perform(elements, appState, _, app) {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true
})[0];
const editingLinearElement = appState.editingLinearElement?.elementId === selectedElement.id ? null : new LinearElementEditor(selectedElement);
return {
appState: {
...appState,
editingLinearElement
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
PanelComponent: ({ appState, updateData, app }) => {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds
})[0];
const label = t(
selectedElement.type === "arrow" ? "labels.lineEditor.editArrow" : "labels.lineEditor.edit"
);
return /* @__PURE__ */ jsx73(
ToolButton,
{
type: "button",
icon: lineEditorIcon,
title: label,
"aria-label": label,
onClick: () => updateData(null)
}
);
}
});
// actions/actionToggleSearchMenu.ts
var actionToggleSearchMenu = register({
name: "searchMenu",
icon: searchIcon,
keywords: ["search", "find"],
label: "search.title",
viewMode: true,
trackEvent: {
category: "search_menu",
action: "toggle",
predicate: (appState) => appState.gridModeEnabled
},
perform(elements, appState, _, app) {
if (appState.openSidebar?.name === DEFAULT_SIDEBAR.name && appState.openSidebar.tab === CANVAS_SEARCH_TAB) {
const searchInput = app.excalidrawContainerValue.container?.querySelector(
`.${CLASSES.SEARCH_MENU_INPUT_WRAPPER} input`
);
if (searchInput?.matches(":focus")) {
return {
appState: { ...appState, openSidebar: null },
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
}
searchInput?.focus();
searchInput?.select();
return false;
}
return {
appState: {
...appState,
openSidebar: { name: DEFAULT_SIDEBAR.name, tab: CANVAS_SEARCH_TAB },
openDialog: null
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
checked: (appState) => appState.gridModeEnabled,
predicate: (element, appState, props) => {
return props.gridModeEnabled === void 0;
},
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.F
});
// actions/actionCropEditor.tsx
import { jsx as jsx74 } from "react/jsx-runtime";
var actionToggleCropEditor = register({
name: "cropEditor",
label: "helpDialog.cropStart",
icon: cropIcon,
viewMode: true,
trackEvent: { category: "menu" },
keywords: ["image", "crop"],
perform(elements, appState, _, app) {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true
})[0];
return {
appState: {
...appState,
isCropping: false,
croppingElementId: selectedElement.id
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
},
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
if (!appState.croppingElementId && selectedElements.length === 1 && isImageElement(selectedElements[0])) {
return true;
}
return false;
},
PanelComponent: ({ appState, updateData, app }) => {
const label = t("helpDialog.cropStart");
return /* @__PURE__ */ jsx74(
ToolButton,
{
type: "button",
icon: cropIcon,
title: label,
"aria-label": label,
onClick: () => updateData(null)
}
);
}
});
// history.ts
var HistoryChangedEvent = class {
constructor(isUndoStackEmpty = true, isRedoStackEmpty = true) {
this.isUndoStackEmpty = isUndoStackEmpty;
this.isRedoStackEmpty = isRedoStackEmpty;
}
};
var History = class _History {
constructor() {
__publicField(this, "onHistoryChangedEmitter", new Emitter());
__publicField(this, "undoStack", []);
__publicField(this, "redoStack", []);
}
get isUndoStackEmpty() {
return this.undoStack.length === 0;
}
get isRedoStackEmpty() {
return this.redoStack.length === 0;
}
clear() {
this.undoStack.length = 0;
this.redoStack.length = 0;
}
/**
* Record a local change which will go into the history
*/
record(elementsChange, appStateChange) {
const entry = HistoryEntry.create(appStateChange, elementsChange);
if (!entry.isEmpty()) {
this.undoStack.push(entry.inverse());
if (!entry.elementsChange.isEmpty()) {
this.redoStack.length = 0;
}
this.onHistoryChangedEmitter.trigger(
new HistoryChangedEvent(this.isUndoStackEmpty, this.isRedoStackEmpty)
);
}
}
undo(elements, appState, snapshot) {
return this.perform(
elements,
appState,
snapshot,
() => _History.pop(this.undoStack),
(entry) => _History.push(this.redoStack, entry, elements)
);
}
redo(elements, appState, snapshot) {
return this.perform(
elements,
appState,
snapshot,
() => _History.pop(this.redoStack),
(entry) => _History.push(this.undoStack, entry, elements)
);
}
perform(elements, appState, snapshot, pop, push) {
try {
let historyEntry = pop();
if (historyEntry === null) {
return;
}
let nextElements = elements;
let nextAppState = appState;
let containsVisibleChange = false;
while (historyEntry) {
try {
[nextElements, nextAppState, containsVisibleChange] = historyEntry.applyTo(nextElements, nextAppState, snapshot);
} finally {
push(historyEntry);
}
if (containsVisibleChange) {
break;
}
historyEntry = pop();
}
return [nextElements, nextAppState];
} finally {
this.onHistoryChangedEmitter.trigger(
new HistoryChangedEvent(this.isUndoStackEmpty, this.isRedoStackEmpty)
);
}
}
static pop(stack) {
if (!stack.length) {
return null;
}
const entry = stack.pop();
if (entry !== void 0) {
return entry;
}
return null;
}
static push(stack, entry, prevElements) {
const updatedEntry = entry.inverse().applyLatestChanges(prevElements);
return stack.push(updatedEntry);
}
};
var HistoryEntry = class _HistoryEntry {
constructor(appStateChange, elementsChange) {
this.appStateChange = appStateChange;
this.elementsChange = elementsChange;
}
static create(appStateChange, elementsChange) {
return new _HistoryEntry(appStateChange, elementsChange);
}
inverse() {
return new _HistoryEntry(
this.appStateChange.inverse(),
this.elementsChange.inverse()
);
}
applyTo(elements, appState, snapshot) {
const [nextElements, elementsContainVisibleChange] = this.elementsChange.applyTo(elements, snapshot.elements);
const [nextAppState, appStateContainsVisibleChange] = this.appStateChange.applyTo(appState, nextElements);
const appliedVisibleChanges = elementsContainVisibleChange || appStateContainsVisibleChange;
return [nextElements, nextAppState, appliedVisibleChanges];
}
/**
* Apply latest (remote) changes to the history entry, creates new instance of `HistoryEntry`.
*/
applyLatestChanges(elements) {
const updatedElementsChange = this.elementsChange.applyLatestChanges(elements);
return _HistoryEntry.create(this.appStateChange, updatedElementsChange);
}
isEmpty() {
return this.appStateChange.isEmpty() && this.elementsChange.isEmpty();
}
};
// hooks/useEmitter.ts
import { useEffect as useEffect29, useState as useState24 } from "react";
var useEmitter = (emitter, initialState) => {
const [event, setEvent] = useState24(initialState);
useEffect29(() => {
const unsubscribe = emitter.on((event2) => {
setEvent(event2);
});
return () => {
unsubscribe();
};
}, [emitter]);
return event;
};
// actions/actionHistory.tsx
import { jsx as jsx75 } from "react/jsx-runtime";
var executeHistoryAction = (app, appState, updater) => {
if (!appState.multiElement && !appState.resizingElement && !appState.editingTextElement && !appState.newElement && !appState.selectedElementsAreBeingDragged && !appState.selectionElement && !app.flowChartCreator.isCreatingChart) {
const result = updater();
if (!result) {
return { captureUpdate: CaptureUpdateAction.EVENTUALLY };
}
const [nextElementsMap, nextAppState] = result;
const nextElements = Array.from(nextElementsMap.values());
return {
appState: nextAppState,
elements: nextElements,
captureUpdate: CaptureUpdateAction.NEVER
};
}
return { captureUpdate: CaptureUpdateAction.EVENTUALLY };
};
var createUndoAction = (history, store) => ({
name: "undo",
label: "buttons.undo",
icon: UndoIcon,
trackEvent: { category: "history" },
viewMode: false,
perform: (elements, appState, value, app) => executeHistoryAction(
app,
appState,
() => history.undo(
arrayToMap(elements),
// TODO: #7348 refactor action manager to already include `SceneElementsMap`
appState,
store.snapshot
)
),
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && matchKey(event, KEYS.Z) && !event.shiftKey,
PanelComponent: ({ updateData, data }) => {
const { isUndoStackEmpty } = useEmitter(
history.onHistoryChangedEmitter,
new HistoryChangedEvent(
history.isUndoStackEmpty,
history.isRedoStackEmpty
)
);
return /* @__PURE__ */ jsx75(
ToolButton,
{
type: "button",
icon: UndoIcon,
"aria-label": t("buttons.undo"),
onClick: updateData,
size: data?.size || "medium",
disabled: isUndoStackEmpty,
"data-testid": "button-undo"
}
);
}
});
var createRedoAction = (history, store) => ({
name: "redo",
label: "buttons.redo",
icon: RedoIcon,
trackEvent: { category: "history" },
viewMode: false,
perform: (elements, appState, _, app) => executeHistoryAction(
app,
appState,
() => history.redo(
arrayToMap(elements),
// TODO: #7348 refactor action manager to already include `SceneElementsMap`
appState,
store.snapshot
)
),
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.shiftKey && matchKey(event, KEYS.Z) || isWindows && event.ctrlKey && !event.shiftKey && matchKey(event, KEYS.Y),
PanelComponent: ({ updateData, data }) => {
const { isRedoStackEmpty } = useEmitter(
history.onHistoryChangedEmitter,
new HistoryChangedEvent(
history.isUndoStackEmpty,
history.isRedoStackEmpty
)
);
return /* @__PURE__ */ jsx75(
ToolButton,
{
type: "button",
icon: RedoIcon,
"aria-label": t("buttons.redo"),
onClick: updateData,
size: data?.size || "medium",
disabled: isRedoStackEmpty,
"data-testid": "button-redo"
}
);
}
});
// actions/manager.tsx
import { jsx as jsx76 } from "react/jsx-runtime";
var trackAction = (action, source, appState, elements, app, value) => {
if (action.trackEvent) {
try {
if (typeof action.trackEvent === "object") {
const shouldTrack = action.trackEvent.predicate ? action.trackEvent.predicate(appState, elements, value) : true;
if (shouldTrack) {
trackEvent(
action.trackEvent.category,
action.trackEvent.action || action.name,
`${source} (${app.device.editor.isMobile ? "mobile" : "desktop"})`
);
}
}
} catch (error) {
console.error("error while logging action:", error);
}
}
};
var ActionManager = class {
constructor(updater, getAppState, getElementsIncludingDeleted, app) {
__publicField(this, "actions", {});
__publicField(this, "updater");
__publicField(this, "getAppState");
__publicField(this, "getElementsIncludingDeleted");
__publicField(this, "app");
/**
* @param data additional data sent to the PanelComponent
*/
__publicField(this, "renderAction", (name, data) => {
const canvasActions = this.app.props.UIOptions.canvasActions;
if (this.actions[name] && "PanelComponent" in this.actions[name] && (name in canvasActions ? canvasActions[name] : true)) {
const action = this.actions[name];
const PanelComponent = action.PanelComponent;
PanelComponent.displayName = "PanelComponent";
const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState();
const updateData = (formState) => {
trackAction(action, "ui", appState, elements, this.app, formState);
this.updater(
action.perform(
this.getElementsIncludingDeleted(),
this.getAppState(),
formState,
this.app
)
);
};
return /* @__PURE__ */ jsx76(
PanelComponent,
{
elements: this.getElementsIncludingDeleted(),
appState: this.getAppState(),
updateData,
appProps: this.app.props,
app: this.app,
data
}
);
}
return null;
});
__publicField(this, "isActionEnabled", (action) => {
const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState();
return !action.predicate || action.predicate(elements, appState, this.app.props, this.app);
});
this.updater = (actionResult) => {
if (isPromiseLike(actionResult)) {
actionResult.then((actionResult2) => {
return updater(actionResult2);
});
} else {
return updater(actionResult);
}
};
this.getAppState = getAppState;
this.getElementsIncludingDeleted = getElementsIncludingDeleted;
this.app = app;
}
registerAction(action) {
this.actions[action.name] = action;
}
registerAll(actions2) {
actions2.forEach((action) => this.registerAction(action));
}
handleKeyDown(event) {
const canvasActions = this.app.props.UIOptions.canvasActions;
const data = Object.values(this.actions).sort((a, b) => (b.keyPriority || 0) - (a.keyPriority || 0)).filter(
(action2) => (action2.name in canvasActions ? canvasActions[action2.name] : true) && action2.keyTest && action2.keyTest(
event,
this.getAppState(),
this.getElementsIncludingDeleted(),
this.app
)
);
if (data.length !== 1) {
if (data.length > 1) {
console.warn("Canceling as multiple actions match this shortcut", data);
}
return false;
}
const action = data[0];
if (this.getAppState().viewModeEnabled && action.viewMode !== true) {
return false;
}
const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState();
const value = null;
trackAction(action, "keyboard", appState, elements, this.app, null);
event.preventDefault();
event.stopPropagation();
this.updater(data[0].perform(elements, appState, value, this.app));
return true;
}
executeAction(action, source = "api", value = null) {
const elements = this.getElementsIncludingDeleted();
const appState = this.getAppState();
trackAction(action, source, appState, elements, this.app, value);
this.updater(action.perform(elements, appState, value, this.app));
}
};
// gesture.ts
var getCenter = (pointers) => {
const allCoords = Array.from(pointers.values());
return {
x: sum(allCoords, (coords) => coords.x) / allCoords.length,
y: sum(allCoords, (coords) => coords.y) / allCoords.length
};
};
var getDistance = ([a, b]) => Math.hypot(a.x - b.x, a.y - b.y);
var sum = (array, mapper) => array.reduce((acc, item) => acc + mapper(item), 0);
// components/ContextMenu.tsx
import clsx33 from "clsx";
// components/Popover.tsx
import { useLayoutEffect as useLayoutEffect4, useRef as useRef23, useEffect as useEffect30 } from "react";
import { unstable_batchedUpdates } from "react-dom";
import { jsx as jsx77 } from "react/jsx-runtime";
var Popover6 = ({
children,
left,
top,
onCloseRequest,
fitInViewport = false,
offsetLeft = 0,
offsetTop = 0,
viewportWidth = window.innerWidth,
viewportHeight = window.innerHeight
}) => {
const popoverRef = useRef23(null);
useEffect30(() => {
const container = popoverRef.current;
if (!container) {
return;
}
if (!container.contains(document.activeElement)) {
container.focus();
}
const handleKeyDown = (event) => {
if (event.key === KEYS.TAB) {
const focusableElements = queryFocusableElements(container);
const { activeElement } = document;
const currentIndex = focusableElements.findIndex(
(element) => element === activeElement
);
if (activeElement === container) {
if (event.shiftKey) {
focusableElements[focusableElements.length - 1]?.focus();
} else {
focusableElements[0].focus();
}
event.preventDefault();
event.stopImmediatePropagation();
} else if (currentIndex === 0 && event.shiftKey) {
focusableElements[focusableElements.length - 1]?.focus();
event.preventDefault();
event.stopImmediatePropagation();
} else if (currentIndex === focusableElements.length - 1 && !event.shiftKey) {
focusableElements[0]?.focus();
event.preventDefault();
event.stopImmediatePropagation();
}
}
};
container.addEventListener("keydown", handleKeyDown);
return () => container.removeEventListener("keydown", handleKeyDown);
}, []);
const lastInitializedPosRef = useRef23(
null
);
useLayoutEffect4(() => {
if (fitInViewport && popoverRef.current && top != null && left != null) {
const container = popoverRef.current;
const { width, height } = container.getBoundingClientRect();
if (lastInitializedPosRef.current?.top === top && lastInitializedPosRef.current?.left === left) {
return;
}
lastInitializedPosRef.current = { top, left };
if (width >= viewportWidth) {
container.style.width = `${viewportWidth}px`;
container.style.left = "0px";
container.style.overflowX = "scroll";
} else if (left + width - offsetLeft > viewportWidth) {
container.style.left = `${viewportWidth - width - 10}px`;
} else {
container.style.left = `${left}px`;
}
if (height >= viewportHeight) {
container.style.height = `${viewportHeight - 20}px`;
container.style.top = "10px";
container.style.overflowY = "scroll";
} else if (top + height - offsetTop > viewportHeight) {
container.style.top = `${viewportHeight - height}px`;
} else {
container.style.top = `${top}px`;
}
}
}, [
top,
left,
fitInViewport,
viewportWidth,
viewportHeight,
offsetLeft,
offsetTop
]);
useEffect30(() => {
if (onCloseRequest) {
const handler = (event) => {
if (!popoverRef.current?.contains(event.target)) {
unstable_batchedUpdates(() => onCloseRequest(event));
}
};
document.addEventListener("pointerdown", handler, false);
return () => document.removeEventListener("pointerdown", handler, false);
}
}, [onCloseRequest]);
return /* @__PURE__ */ jsx77("div", { className: "popover", ref: popoverRef, tabIndex: -1, children });
};
// components/ContextMenu.tsx
import React29 from "react";
import { jsx as jsx78, jsxs as jsxs42 } from "react/jsx-runtime";
var CONTEXT_MENU_SEPARATOR = "separator";
var ContextMenu = React29.memo(
({ actionManager, items, top, left, onClose }) => {
const appState = useExcalidrawAppState();
const elements = useExcalidrawElements();
const filteredItems = items.reduce((acc, item) => {
if (item && (item === CONTEXT_MENU_SEPARATOR || !item.predicate || item.predicate(
elements,
appState,
actionManager.app.props,
actionManager.app
))) {
acc.push(item);
}
return acc;
}, []);
return /* @__PURE__ */ jsx78(
Popover6,
{
onCloseRequest: () => {
onClose();
},
top,
left,
fitInViewport: true,
offsetLeft: appState.offsetLeft,
offsetTop: appState.offsetTop,
viewportWidth: appState.width,
viewportHeight: appState.height,
children: /* @__PURE__ */ jsx78(
"ul",
{
className: "context-menu",
onContextMenu: (event) => event.preventDefault(),
children: filteredItems.map((item, idx) => {
if (item === CONTEXT_MENU_SEPARATOR) {
if (!filteredItems[idx - 1] || filteredItems[idx - 1] === CONTEXT_MENU_SEPARATOR) {
return null;
}
return /* @__PURE__ */ jsx78("hr", { className: "context-menu-item-separator" }, idx);
}
const actionName = item.name;
let label = "";
if (item.label) {
if (typeof item.label === "function") {
label = t(
item.label(
elements,
appState,
actionManager.app
)
);
} else {
label = t(item.label);
}
}
return /* @__PURE__ */ jsx78(
"li",
{
"data-testid": actionName,
onClick: () => {
onClose(() => {
actionManager.executeAction(item, "contextMenu");
});
},
children: /* @__PURE__ */ jsxs42(
"button",
{
type: "button",
className: clsx33("context-menu-item", {
dangerous: actionName === "deleteSelectedElements",
checkmark: item.checked?.(appState)
}),
children: [
/* @__PURE__ */ jsx78("div", { className: "context-menu-item__label", children: label }),
/* @__PURE__ */ jsx78("kbd", { className: "context-menu-item__shortcut", children: actionName ? getShortcutFromShortcutName(actionName) : "" })
]
}
)
},
idx
);
})
}
)
}
);
}
);
// components/LayerUI.tsx
import clsx53 from "clsx";
import React40 from "react";
// components/ErrorDialog.tsx
import React30, { useState as useState25 } from "react";
import { Fragment as Fragment10, jsx as jsx79 } from "react/jsx-runtime";
var ErrorDialog = ({
children,
onClose
}) => {
const [modalIsShown, setModalIsShown] = useState25(!!children);
const { container: excalidrawContainer } = useExcalidrawContainer();
const handleClose = React30.useCallback(() => {
setModalIsShown(false);
if (onClose) {
onClose();
}
excalidrawContainer?.focus();
}, [onClose, excalidrawContainer]);
return /* @__PURE__ */ jsx79(Fragment10, { children: modalIsShown && /* @__PURE__ */ jsx79(
Dialog,
{
size: "small",
onCloseRequest: handleClose,
title: t("errorDialog.title"),
children: /* @__PURE__ */ jsx79("div", { style: { whiteSpace: "pre-wrap" }, children })
}
) });
};
// components/ImageExportDialog.tsx
import { useEffect as useEffect31, useRef as useRef25, useState as useState28 } from "react";
// components/RadioGroup.tsx
import clsx34 from "clsx";
import { jsx as jsx80, jsxs as jsxs43 } from "react/jsx-runtime";
var RadioGroup = function({
onChange,
value,
choices,
name
}) {
return /* @__PURE__ */ jsx80("div", { className: "RadioGroup", children: choices.map((choice) => /* @__PURE__ */ jsxs43(
"div",
{
className: clsx34("RadioGroup__choice", {
active: choice.value === value
}),
title: choice.ariaLabel,
children: [
/* @__PURE__ */ jsx80(
"input",
{
name,
type: "radio",
checked: choice.value === value,
onChange: () => onChange(choice.value),
"aria-label": choice.ariaLabel
}
),
choice.label
]
},
String(choice.value)
)) });
};
// components/Switch.tsx
import clsx35 from "clsx";
import { jsx as jsx81 } from "react/jsx-runtime";
var Switch = ({
title,
name,
checked,
onChange,
disabled = false
}) => {
return /* @__PURE__ */ jsx81("div", { className: clsx35("Switch", { toggled: checked, disabled }), children: /* @__PURE__ */ jsx81(
"input",
{
name,
id: name,
title,
type: "checkbox",
checked,
disabled,
onChange: () => onChange(!checked),
onKeyDown: (event) => {
if (event.key === " ") {
onChange(!checked);
}
}
}
) });
};
// components/FilledButton.tsx
import { forwardRef as forwardRef4, useState as useState26 } from "react";
import clsx36 from "clsx";
import { jsx as jsx82, jsxs as jsxs44 } from "react/jsx-runtime";
var FilledButton = forwardRef4(
({
children,
icon,
onClick,
label,
variant = "filled",
color = "primary",
size = "medium",
fullWidth,
className,
status
}, ref) => {
const [isLoading, setIsLoading] = useState26(false);
const _onClick = async (event) => {
const ret = onClick?.(event);
if (isPromiseLike(ret)) {
const timer = window.setTimeout(() => {
setIsLoading(true);
}, 50);
try {
await ret;
} catch (error) {
if (!(error instanceof AbortError)) {
throw error;
} else {
console.warn(error);
}
} finally {
clearTimeout(timer);
setIsLoading(false);
}
}
};
const _status = isLoading ? "loading" : status;
color = _status === "success" ? "success" : color;
return /* @__PURE__ */ jsx82(
"button",
{
className: clsx36(
"ExcButton",
`ExcButton--color-${color}`,
`ExcButton--variant-${variant}`,
`ExcButton--size-${size}`,
`ExcButton--status-${_status}`,
{ "ExcButton--fullWidth": fullWidth },
className
),
onClick: _onClick,
type: "button",
"aria-label": label,
ref,
disabled: _status === "loading" || _status === "success",
children: /* @__PURE__ */ jsxs44("div", { className: "ExcButton__contents", children: [
_status === "loading" ? /* @__PURE__ */ jsx82(Spinner_default, { className: "ExcButton__statusIcon" }) : _status === "success" && /* @__PURE__ */ jsx82("div", { className: "ExcButton__statusIcon", children: tablerCheckIcon }),
icon && /* @__PURE__ */ jsx82("div", { className: "ExcButton__icon", "aria-hidden": true, children: icon }),
variant !== "icon" && (children ?? label)
] })
}
);
}
);
// hooks/useCopiedIndicator.ts
import { useCallback as useCallback11, useRef as useRef24, useState as useState27 } from "react";
var TIMEOUT = 2e3;
var useCopyStatus = () => {
const [copyStatus, setCopyStatus] = useState27(null);
const timeoutRef = useRef24(0);
const onCopy = () => {
clearTimeout(timeoutRef.current);
setCopyStatus("success");
timeoutRef.current = window.setTimeout(() => {
setCopyStatus(null);
}, TIMEOUT);
};
const resetCopyStatus = useCallback11(() => {
setCopyStatus(null);
}, []);
return {
copyStatus,
resetCopyStatus,
onCopy
};
};
// components/ImageExportDialog.tsx
import { jsx as jsx83, jsxs as jsxs45 } from "react/jsx-runtime";
var supportsContextFilters = "filter" in document.createElement("canvas").getContext("2d");
var ErrorCanvasPreview = () => {
return /* @__PURE__ */ jsxs45("div", { children: [
/* @__PURE__ */ jsx83("h3", { children: t("canvasError.cannotShowPreview") }),
/* @__PURE__ */ jsx83("p", { children: /* @__PURE__ */ jsx83("span", { children: t("canvasError.canvasTooBig") }) }),
/* @__PURE__ */ jsxs45("em", { children: [
"(",
t("canvasError.canvasTooBigTip"),
")"
] })
] });
};
var ImageExportModal = ({
appStateSnapshot,
elementsSnapshot,
files,
actionManager,
onExportImage,
name
}) => {
const hasSelection = isSomeElementSelected(
elementsSnapshot,
appStateSnapshot
);
const [projectName, setProjectName] = useState28(name);
const [exportSelectionOnly, setExportSelectionOnly] = useState28(hasSelection);
const [exportWithBackground, setExportWithBackground] = useState28(
appStateSnapshot.exportBackground
);
const [exportDarkMode, setExportDarkMode] = useState28(
appStateSnapshot.exportWithDarkMode
);
const [embedScene, setEmbedScene] = useState28(
appStateSnapshot.exportEmbedScene
);
const [exportScale, setExportScale] = useState28(appStateSnapshot.exportScale);
const previewRef = useRef25(null);
const [renderError, setRenderError] = useState28(null);
const { onCopy, copyStatus, resetCopyStatus } = useCopyStatus();
useEffect31(() => {
resetCopyStatus();
}, [
projectName,
exportWithBackground,
exportDarkMode,
exportScale,
embedScene,
resetCopyStatus
]);
const { exportedElements, exportingFrame } = prepareElementsForExport(
elementsSnapshot,
appStateSnapshot,
exportSelectionOnly
);
useEffect31(() => {
const previewNode = previewRef.current;
if (!previewNode) {
return;
}
const maxWidth = previewNode.offsetWidth;
const maxHeight = previewNode.offsetHeight;
if (!maxWidth) {
return;
}
exportToCanvas2({
elements: exportedElements,
appState: {
...appStateSnapshot,
name: projectName,
exportBackground: exportWithBackground,
exportWithDarkMode: exportDarkMode,
exportScale,
exportEmbedScene: embedScene
},
files,
exportPadding: DEFAULT_EXPORT_PADDING,
maxWidthOrHeight: Math.max(maxWidth, maxHeight),
exportingFrame
}).then((canvas) => {
setRenderError(null);
return canvasToBlob(canvas).then(() => {
previewNode.replaceChildren(canvas);
}).catch((e) => {
if (e.name === "CANVAS_POSSIBLY_TOO_BIG") {
throw new Error(t("canvasError.canvasTooBig"));
}
throw e;
});
}).catch((error) => {
console.error(error);
setRenderError(error);
});
}, [
appStateSnapshot,
files,
exportedElements,
exportingFrame,
projectName,
exportWithBackground,
exportDarkMode,
exportScale,
embedScene
]);
return /* @__PURE__ */ jsxs45("div", { className: "ImageExportModal", children: [
/* @__PURE__ */ jsx83("h3", { children: t("imageExportDialog.header") }),
/* @__PURE__ */ jsxs45("div", { className: "ImageExportModal__preview", children: [
/* @__PURE__ */ jsx83("div", { className: "ImageExportModal__preview__canvas", ref: previewRef, children: renderError && /* @__PURE__ */ jsx83(ErrorCanvasPreview, {}) }),
/* @__PURE__ */ jsx83("div", { className: "ImageExportModal__preview__filename", children: !nativeFileSystemSupported && /* @__PURE__ */ jsx83(
"input",
{
type: "text",
className: "TextInput",
value: projectName,
style: { width: "30ch" },
onChange: (event) => {
setProjectName(event.target.value);
actionManager.executeAction(
actionChangeProjectName,
"ui",
event.target.value
);
}
}
) })
] }),
/* @__PURE__ */ jsxs45("div", { className: "ImageExportModal__settings", children: [
/* @__PURE__ */ jsx83("h3", { children: t("imageExportDialog.header") }),
hasSelection && /* @__PURE__ */ jsx83(
ExportSetting,
{
label: t("imageExportDialog.label.onlySelected"),
name: "exportOnlySelected",
children: /* @__PURE__ */ jsx83(
Switch,
{
name: "exportOnlySelected",
checked: exportSelectionOnly,
onChange: (checked) => {
setExportSelectionOnly(checked);
}
}
)
}
),
/* @__PURE__ */ jsx83(
ExportSetting,
{
label: t("imageExportDialog.label.withBackground"),
name: "exportBackgroundSwitch",
children: /* @__PURE__ */ jsx83(
Switch,
{
name: "exportBackgroundSwitch",
checked: exportWithBackground,
onChange: (checked) => {
setExportWithBackground(checked);
actionManager.executeAction(
actionChangeExportBackground,
"ui",
checked
);
}
}
)
}
),
supportsContextFilters && /* @__PURE__ */ jsx83(
ExportSetting,
{
label: t("imageExportDialog.label.darkMode"),
name: "exportDarkModeSwitch",
children: /* @__PURE__ */ jsx83(
Switch,
{
name: "exportDarkModeSwitch",
checked: exportDarkMode,
onChange: (checked) => {
setExportDarkMode(checked);
actionManager.executeAction(
actionExportWithDarkMode,
"ui",
checked
);
}
}
)
}
),
/* @__PURE__ */ jsx83(
ExportSetting,
{
label: t("imageExportDialog.label.embedScene"),
tooltip: t("imageExportDialog.tooltip.embedScene"),
name: "exportEmbedSwitch",
children: /* @__PURE__ */ jsx83(
Switch,
{
name: "exportEmbedSwitch",
checked: embedScene,
onChange: (checked) => {
setEmbedScene(checked);
actionManager.executeAction(
actionChangeExportEmbedScene,
"ui",
checked
);
}
}
)
}
),
/* @__PURE__ */ jsx83(
ExportSetting,
{
label: t("imageExportDialog.label.scale"),
name: "exportScale",
children: /* @__PURE__ */ jsx83(
RadioGroup,
{
name: "exportScale",
value: exportScale,
onChange: (scale) => {
setExportScale(scale);
actionManager.executeAction(actionChangeExportScale, "ui", scale);
},
choices: EXPORT_SCALES.map((scale) => ({
value: scale,
label: `${scale}\xD7`
}))
}
)
}
),
/* @__PURE__ */ jsxs45("div", { className: "ImageExportModal__settings__buttons", children: [
/* @__PURE__ */ jsx83(
FilledButton,
{
className: "ImageExportModal__settings__buttons__button",
label: t("imageExportDialog.title.exportToPng"),
onClick: () => onExportImage(EXPORT_IMAGE_TYPES.png, exportedElements, {
exportingFrame
}),
icon: downloadIcon,
children: t("imageExportDialog.button.exportToPng")
}
),
/* @__PURE__ */ jsx83(
FilledButton,
{
className: "ImageExportModal__settings__buttons__button",
label: t("imageExportDialog.title.exportToSvg"),
onClick: () => onExportImage(EXPORT_IMAGE_TYPES.svg, exportedElements, {
exportingFrame
}),
icon: downloadIcon,
children: t("imageExportDialog.button.exportToSvg")
}
),
(probablySupportsClipboardBlob || isFirefox) && /* @__PURE__ */ jsx83(
FilledButton,
{
className: "ImageExportModal__settings__buttons__button",
label: t("imageExportDialog.title.copyPngToClipboard"),
status: copyStatus,
onClick: async () => {
await onExportImage(
EXPORT_IMAGE_TYPES.clipboard,
exportedElements,
{
exportingFrame
}
);
onCopy();
},
icon: copyIcon,
children: t("imageExportDialog.button.copyPngToClipboard")
}
)
] })
] })
] });
};
var ExportSetting = ({
label,
children,
tooltip,
name
}) => {
return /* @__PURE__ */ jsxs45("div", { className: "ImageExportModal__settings__setting", title: label, children: [
/* @__PURE__ */ jsxs45(
"label",
{
htmlFor: name,
className: "ImageExportModal__settings__setting__label",
children: [
label,
tooltip && /* @__PURE__ */ jsx83(Tooltip, { label: tooltip, long: true, children: helpIcon })
]
}
),
/* @__PURE__ */ jsx83("div", { className: "ImageExportModal__settings__setting__content", children })
] });
};
var ImageExportDialog = ({
elements,
appState,
files,
actionManager,
onExportImage,
onCloseRequest,
name
}) => {
const [{ appStateSnapshot, elementsSnapshot }] = useState28(() => {
return {
appStateSnapshot: cloneJSON(appState),
elementsSnapshot: cloneJSON(elements)
};
});
return /* @__PURE__ */ jsx83(Dialog, { onCloseRequest, size: "wide", title: false, children: /* @__PURE__ */ jsx83(
ImageExportModal,
{
elementsSnapshot,
appStateSnapshot,
files,
actionManager,
onExportImage,
name
}
) });
};
// components/FixedSideContainer.tsx
import clsx37 from "clsx";
import { jsx as jsx84 } from "react/jsx-runtime";
var FixedSideContainer = ({
children,
side,
className
}) => /* @__PURE__ */ jsx84(
"div",
{
className: clsx37(
"FixedSideContainer",
`FixedSideContainer_side_${side}`,
className
),
children
}
);
// element/flowchart.ts
var VERTICAL_OFFSET = 100;
var HORIZONTAL_OFFSET = 100;
var getLinkDirectionFromKey = (key) => {
switch (key) {
case KEYS.ARROW_UP:
return "up";
case KEYS.ARROW_DOWN:
return "down";
case KEYS.ARROW_RIGHT:
return "right";
case KEYS.ARROW_LEFT:
return "left";
default:
return "right";
}
};
var getNodeRelatives = (type, node, elementsMap, direction) => {
const items = [...elementsMap.values()].reduce(
(acc, el) => {
let oppositeBinding;
if (isElbowArrow(el) && // we want check existence of the opposite binding, in the direction
// we're interested in
(oppositeBinding = el[type === "predecessors" ? "startBinding" : "endBinding"]) && // similarly, we need to filter only arrows bound to target node
el[type === "predecessors" ? "endBinding" : "startBinding"]?.elementId === node.id) {
const relative = elementsMap.get(oppositeBinding.elementId);
if (!relative) {
return acc;
}
invariant(
isBindableElement(relative),
"not an ExcalidrawBindableElement"
);
const edgePoint = type === "predecessors" ? el.points[el.points.length - 1] : [0, 0];
const heading = headingForPointFromElement(node, aabbForElement(node), [
edgePoint[0] + el.x,
edgePoint[1] + el.y
]);
acc.push({
relative,
heading
});
}
return acc;
},
[]
);
switch (direction) {
case "up":
return items.filter((item) => compareHeading(item.heading, HEADING_UP)).map((item) => item.relative);
case "down":
return items.filter((item) => compareHeading(item.heading, HEADING_DOWN)).map((item) => item.relative);
case "right":
return items.filter((item) => compareHeading(item.heading, HEADING_RIGHT)).map((item) => item.relative);
case "left":
return items.filter((item) => compareHeading(item.heading, HEADING_LEFT)).map((item) => item.relative);
}
};
var getSuccessors = (node, elementsMap, direction) => {
return getNodeRelatives("successors", node, elementsMap, direction);
};
var getPredecessors = (node, elementsMap, direction) => {
return getNodeRelatives("predecessors", node, elementsMap, direction);
};
var getOffsets = (element, linkedNodes, direction) => {
const _HORIZONTAL_OFFSET = HORIZONTAL_OFFSET + element.width;
if (direction === "up" || direction === "down") {
const _VERTICAL_OFFSET2 = VERTICAL_OFFSET + element.height;
const minX = element.x;
const maxX = element.x + element.width;
if (linkedNodes.every(
(linkedNode) => linkedNode.x + linkedNode.width < minX || linkedNode.x > maxX
)) {
return {
x: 0,
y: _VERTICAL_OFFSET2 * (direction === "up" ? -1 : 1)
};
}
} else if (direction === "right" || direction === "left") {
const minY = element.y;
const maxY = element.y + element.height;
if (linkedNodes.every(
(linkedNode) => linkedNode.y + linkedNode.height < minY || linkedNode.y > maxY
)) {
return {
x: (HORIZONTAL_OFFSET + element.width) * (direction === "left" ? -1 : 1),
y: 0
};
}
}
if (direction === "up" || direction === "down") {
const _VERTICAL_OFFSET2 = VERTICAL_OFFSET + element.height;
const y2 = linkedNodes.length === 0 ? _VERTICAL_OFFSET2 : _VERTICAL_OFFSET2;
const x2 = linkedNodes.length === 0 ? 0 : (linkedNodes.length + 1) % 2 === 0 ? (linkedNodes.length + 1) / 2 * _HORIZONTAL_OFFSET : linkedNodes.length / 2 * _HORIZONTAL_OFFSET * -1;
if (direction === "up") {
return {
x: x2,
y: y2 * -1
};
}
return {
x: x2,
y: y2
};
}
const _VERTICAL_OFFSET = VERTICAL_OFFSET + element.height;
const x = (linkedNodes.length === 0 ? HORIZONTAL_OFFSET : HORIZONTAL_OFFSET) + element.width;
const y = linkedNodes.length === 0 ? 0 : (linkedNodes.length + 1) % 2 === 0 ? (linkedNodes.length + 1) / 2 * _VERTICAL_OFFSET : linkedNodes.length / 2 * _VERTICAL_OFFSET * -1;
if (direction === "left") {
return {
x: x * -1,
y
};
}
return {
x,
y
};
};
var addNewNode = (element, elementsMap, appState, direction) => {
const successors = getSuccessors(element, elementsMap, direction);
const predeccessors = getPredecessors(element, elementsMap, direction);
const offsets = getOffsets(
element,
[...successors, ...predeccessors],
direction
);
const nextNode = newElement({
type: element.type,
x: element.x + offsets.x,
y: element.y + offsets.y,
// TODO: extract this to a util
width: element.width,
height: element.height,
roundness: element.roundness,
roughness: element.roughness,
backgroundColor: element.backgroundColor,
strokeColor: element.strokeColor,
strokeWidth: element.strokeWidth,
opacity: element.opacity,
fillStyle: element.fillStyle,
strokeStyle: element.strokeStyle
});
invariant(
isFlowchartNodeElement(nextNode),
"not an ExcalidrawFlowchartNodeElement"
);
const bindingArrow = createBindingArrow(
element,
nextNode,
elementsMap,
direction,
appState
);
return {
nextNode,
bindingArrow
};
};
var addNewNodes = (startNode, elementsMap, appState, direction, numberOfNodes) => {
const newNodes = [];
for (let i = 0; i < numberOfNodes; i++) {
let nextX;
let nextY;
if (direction === "left" || direction === "right") {
const totalHeight = VERTICAL_OFFSET * (numberOfNodes - 1) + numberOfNodes * startNode.height;
const startY = startNode.y + startNode.height / 2 - totalHeight / 2;
let offsetX = HORIZONTAL_OFFSET + startNode.width;
if (direction === "left") {
offsetX *= -1;
}
nextX = startNode.x + offsetX;
const offsetY = (VERTICAL_OFFSET + startNode.height) * i;
nextY = startY + offsetY;
} else {
const totalWidth = HORIZONTAL_OFFSET * (numberOfNodes - 1) + numberOfNodes * startNode.width;
const startX = startNode.x + startNode.width / 2 - totalWidth / 2;
let offsetY = VERTICAL_OFFSET + startNode.height;
if (direction === "up") {
offsetY *= -1;
}
nextY = startNode.y + offsetY;
const offsetX = (HORIZONTAL_OFFSET + startNode.width) * i;
nextX = startX + offsetX;
}
const nextNode = newElement({
type: startNode.type,
x: nextX,
y: nextY,
// TODO: extract this to a util
width: startNode.width,
height: startNode.height,
roundness: startNode.roundness,
roughness: startNode.roughness,
backgroundColor: startNode.backgroundColor,
strokeColor: startNode.strokeColor,
strokeWidth: startNode.strokeWidth,
opacity: startNode.opacity,
fillStyle: startNode.fillStyle,
strokeStyle: startNode.strokeStyle
});
invariant(
isFlowchartNodeElement(nextNode),
"not an ExcalidrawFlowchartNodeElement"
);
const bindingArrow = createBindingArrow(
startNode,
nextNode,
elementsMap,
direction,
appState
);
newNodes.push(nextNode);
newNodes.push(bindingArrow);
}
return newNodes;
};
var createBindingArrow = (startBindingElement, endBindingElement, elementsMap, direction, appState) => {
let startX;
let startY;
const PADDING = 6;
switch (direction) {
case "up": {
startX = startBindingElement.x + startBindingElement.width / 2;
startY = startBindingElement.y - PADDING;
break;
}
case "down": {
startX = startBindingElement.x + startBindingElement.width / 2;
startY = startBindingElement.y + startBindingElement.height + PADDING;
break;
}
case "right": {
startX = startBindingElement.x + startBindingElement.width + PADDING;
startY = startBindingElement.y + startBindingElement.height / 2;
break;
}
case "left": {
startX = startBindingElement.x - PADDING;
startY = startBindingElement.y + startBindingElement.height / 2;
break;
}
}
let endX;
let endY;
switch (direction) {
case "up": {
endX = endBindingElement.x + endBindingElement.width / 2 - startX;
endY = endBindingElement.y + endBindingElement.height - startY + PADDING;
break;
}
case "down": {
endX = endBindingElement.x + endBindingElement.width / 2 - startX;
endY = endBindingElement.y - startY - PADDING;
break;
}
case "right": {
endX = endBindingElement.x - startX - PADDING;
endY = endBindingElement.y - startY + endBindingElement.height / 2;
break;
}
case "left": {
endX = endBindingElement.x + endBindingElement.width - startX + PADDING;
endY = endBindingElement.y - startY + endBindingElement.height / 2;
break;
}
}
const bindingArrow = newArrowElement({
type: "arrow",
x: startX,
y: startY,
startArrowhead: null,
endArrowhead: appState.currentItemEndArrowhead,
strokeColor: startBindingElement.strokeColor,
strokeStyle: startBindingElement.strokeStyle,
strokeWidth: startBindingElement.strokeWidth,
opacity: startBindingElement.opacity,
roughness: startBindingElement.roughness,
points: [pointFrom(0, 0), pointFrom(endX, endY)],
elbowed: true
});
bindLinearElement(
bindingArrow,
startBindingElement,
"start",
elementsMap
);
bindLinearElement(
bindingArrow,
endBindingElement,
"end",
elementsMap
);
const changedElements = /* @__PURE__ */ new Map();
changedElements.set(
startBindingElement.id,
startBindingElement
);
changedElements.set(
endBindingElement.id,
endBindingElement
);
changedElements.set(
bindingArrow.id,
bindingArrow
);
LinearElementEditor.movePoints(bindingArrow, [
{
index: 1,
point: bindingArrow.points[1]
}
]);
const update = updateElbowArrowPoints(
bindingArrow,
toBrandedType(
new Map([
...elementsMap.entries(),
[startBindingElement.id, startBindingElement],
[endBindingElement.id, endBindingElement],
[bindingArrow.id, bindingArrow]
])
),
{ points: bindingArrow.points }
);
return {
...bindingArrow,
...update
};
};
var FlowChartNavigator = class {
constructor() {
__publicField(this, "isExploring", false);
// nodes that are ONE link away (successor and predecessor both included)
__publicField(this, "sameLevelNodes", []);
__publicField(this, "sameLevelIndex", 0);
// set it to the opposite of the defalut creation direction
__publicField(this, "direction", null);
// for speedier navigation
__publicField(this, "visitedNodes", /* @__PURE__ */ new Set());
}
clear() {
this.isExploring = false;
this.sameLevelNodes = [];
this.sameLevelIndex = 0;
this.direction = null;
this.visitedNodes.clear();
}
exploreByDirection(element, elementsMap, direction) {
if (!isBindableElement(element)) {
return null;
}
if (direction !== this.direction) {
this.clear();
}
if (!this.visitedNodes.has(element.id)) {
this.visitedNodes.add(element.id);
}
if (this.isExploring && direction === this.direction && this.sameLevelNodes.length > 1) {
this.sameLevelIndex = (this.sameLevelIndex + 1) % this.sameLevelNodes.length;
return this.sameLevelNodes[this.sameLevelIndex].id;
}
const nodes = [
...getSuccessors(element, elementsMap, direction),
...getPredecessors(element, elementsMap, direction)
];
if (nodes.length > 0) {
this.sameLevelIndex = 0;
this.isExploring = true;
this.sameLevelNodes = nodes;
this.direction = direction;
this.visitedNodes.add(nodes[0].id);
return nodes[0].id;
}
if (direction === this.direction || !this.isExploring) {
if (!this.isExploring) {
this.visitedNodes.add(element.id);
}
const otherDirections = [
"up",
"right",
"down",
"left"
].filter((dir) => dir !== direction);
const otherLinkedNodes = otherDirections.map((dir) => [
...getSuccessors(element, elementsMap, dir),
...getPredecessors(element, elementsMap, dir)
]).flat().filter((linkedNode) => !this.visitedNodes.has(linkedNode.id));
for (const linkedNode of otherLinkedNodes) {
if (!this.visitedNodes.has(linkedNode.id)) {
this.visitedNodes.add(linkedNode.id);
this.isExploring = true;
this.direction = direction;
return linkedNode.id;
}
}
}
return null;
}
};
var FlowChartCreator = class {
constructor() {
__publicField(this, "isCreatingChart", false);
__publicField(this, "numberOfNodes", 0);
__publicField(this, "direction", "right");
__publicField(this, "pendingNodes", null);
}
createNodes(startNode, elementsMap, appState, direction) {
if (direction !== this.direction) {
const { nextNode, bindingArrow } = addNewNode(
startNode,
elementsMap,
appState,
direction
);
this.numberOfNodes = 1;
this.isCreatingChart = true;
this.direction = direction;
this.pendingNodes = [nextNode, bindingArrow];
} else {
this.numberOfNodes += 1;
const newNodes = addNewNodes(
startNode,
elementsMap,
appState,
direction,
this.numberOfNodes
);
this.isCreatingChart = true;
this.direction = direction;
this.pendingNodes = newNodes;
}
if (startNode.frameId) {
const frame = elementsMap.get(startNode.frameId);
invariant(
frame && isFrameElement(frame),
"not an ExcalidrawFrameElement"
);
if (frame && this.pendingNodes.every(
(node) => elementsAreInFrameBounds([node], frame, elementsMap) || elementOverlapsWithFrame(node, frame, elementsMap)
)) {
this.pendingNodes = this.pendingNodes.map(
(node) => mutateElement(
node,
{
frameId: startNode.frameId
},
false
)
);
}
}
}
clear() {
this.isCreatingChart = false;
this.pendingNodes = null;
this.direction = null;
this.numberOfNodes = 0;
}
};
var isNodeInFlowchart = (element, elementsMap) => {
for (const [, el] of elementsMap) {
if (el.type === "arrow" && (el.startBinding?.elementId === element.id || el.endBinding?.elementId === element.id)) {
return true;
}
}
return false;
};
// components/HintViewer.tsx
import { jsx as jsx85 } from "react/jsx-runtime";
var getHints = ({
appState,
isMobile,
device,
app
}) => {
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null;
if (appState.openSidebar?.name === DEFAULT_SIDEBAR.name && appState.openSidebar.tab === CANVAS_SEARCH_TAB && appState.searchMatches?.length) {
return t("hints.dismissSearch");
}
if (appState.openSidebar && !device.editor.canFitSidebar) {
return null;
}
if (isEraserActive(appState)) {
return t("hints.eraserRevert");
}
if (activeTool.type === "arrow" || activeTool.type === "line") {
if (multiMode) {
return t("hints.linearElementMulti");
}
if (activeTool.type === "arrow") {
return t("hints.arrowTool", { arrowShortcut: getShortcutKey("A") });
}
return t("hints.linearElement");
}
if (activeTool.type === "freedraw") {
return t("hints.freeDraw");
}
if (activeTool.type === "text") {
return t("hints.text");
}
if (activeTool.type === "embeddable") {
return t("hints.embeddable");
}
if (appState.activeTool.type === "image" && appState.pendingImageElementId) {
return t("hints.placeImage");
}
const selectedElements = app.scene.getSelectedElements(appState);
if (isResizing && lastPointerDownWith === "mouse" && selectedElements.length === 1) {
const targetElement = selectedElements[0];
if (isLinearElement(targetElement) && targetElement.points.length === 2) {
return t("hints.lockAngle");
}
return isImageElement(targetElement) ? t("hints.resizeImage") : t("hints.resize");
}
if (isRotating && lastPointerDownWith === "mouse") {
return t("hints.rotate");
}
if (selectedElements.length === 1 && isTextElement(selectedElements[0])) {
return t("hints.text_selected");
}
if (appState.editingTextElement) {
return t("hints.text_editing");
}
if (appState.croppingElementId) {
return t("hints.leaveCropEditor");
}
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
return t("hints.enterCropEditor");
}
if (activeTool.type === "selection") {
if (appState.selectionElement && !selectedElements.length && !appState.editingTextElement && !appState.editingLinearElement) {
return t("hints.deepBoxSelect");
}
if (isGridModeEnabled(app) && appState.selectedElementsAreBeingDragged) {
return t("hints.disableSnapping");
}
if (!selectedElements.length && !isMobile) {
return t("hints.canvasPanning");
}
if (selectedElements.length === 1) {
if (isLinearElement(selectedElements[0])) {
if (appState.editingLinearElement) {
return appState.editingLinearElement.selectedPointsIndices ? t("hints.lineEditor_pointSelected") : t("hints.lineEditor_nothingSelected");
}
return t("hints.lineEditor_info");
}
if (!appState.newElement && !appState.selectedElementsAreBeingDragged && isTextBindableContainer(selectedElements[0])) {
if (isFlowchartNodeElement(selectedElements[0])) {
if (isNodeInFlowchart(
selectedElements[0],
app.scene.getNonDeletedElementsMap()
)) {
return [t("hints.bindTextToElement"), t("hints.createFlowchart")];
}
return [t("hints.bindTextToElement"), t("hints.createFlowchart")];
}
return t("hints.bindTextToElement");
}
}
}
return null;
};
var HintViewer = ({
appState,
isMobile,
device,
app
}) => {
const hints = getHints({
appState,
isMobile,
device,
app
});
if (!hints) {
return null;
}
const hint = Array.isArray(hints) ? hints.map((hint2) => {
return getShortcutKey(hint2).replace(/\. ?$/, "");
}).join(". ") : getShortcutKey(hints);
return /* @__PURE__ */ jsx85("div", { className: "HintViewer", children: /* @__PURE__ */ jsx85("span", { children: hint }) });
};
// components/LockButton.tsx
import clsx38 from "clsx";
import { jsx as jsx86, jsxs as jsxs46 } from "react/jsx-runtime";
var DEFAULT_SIZE = "medium";
var ICONS2 = {
CHECKED: LockedIcon,
UNCHECKED: UnlockedIcon
};
var LockButton = (props) => {
return /* @__PURE__ */ jsxs46(
"label",
{
className: clsx38(
"ToolIcon ToolIcon__lock",
`ToolIcon_size_${DEFAULT_SIZE}`,
{
"is-mobile": props.isMobile
}
),
title: `${props.title} \u2014 Q`,
children: [
/* @__PURE__ */ jsx86(
"input",
{
className: "ToolIcon_type_checkbox",
type: "checkbox",
name: props.name,
onChange: props.onChange,
checked: props.checked,
"aria-label": props.title,
"data-testid": "toolbar-lock"
}
),
/* @__PURE__ */ jsx86("div", { className: "ToolIcon__icon", children: props.checked ? ICONS2.CHECKED : ICONS2.UNCHECKED })
]
}
);
};
// components/Section.tsx
import { Fragment as Fragment11, jsx as jsx87, jsxs as jsxs47 } from "react/jsx-runtime";
var Section = ({ heading, children, ...props }) => {
const { id } = useExcalidrawContainer();
const header = /* @__PURE__ */ jsx87("h2", { className: "visually-hidden", id: `${id}-${heading}-title`, children: t(`headings.${heading}`) });
return /* @__PURE__ */ jsx87("section", { ...props, "aria-labelledby": `${id}-${heading}-title`, children: typeof children === "function" ? children(header) : /* @__PURE__ */ jsxs47(Fragment11, { children: [
header,
children
] }) });
};
// scene/scrollbars.ts
var SCROLLBAR_MARGIN = 4;
var SCROLLBAR_WIDTH = 6;
var SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
var getScrollBars = (elements, viewportWidth, viewportHeight, appState) => {
if (!elements.length) {
return {
horizontal: null,
vertical: null
};
}
const [elementsMinX, elementsMinY, elementsMaxX, elementsMaxY] = getCommonBounds(elements);
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
const safeArea = {
top: parseInt(getGlobalCSSVariable("sat")) || 0,
bottom: parseInt(getGlobalCSSVariable("sab")) || 0,
left: parseInt(getGlobalCSSVariable("sal")) || 0,
right: parseInt(getGlobalCSSVariable("sar")) || 0
};
const isRTL = getLanguage().rtl;
const viewportMinX = -appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
const viewportMinY = -appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
const sceneMinX = Math.min(elementsMinX, viewportMinX);
const sceneMinY = Math.min(elementsMinY, viewportMinY);
const sceneMaxX = Math.max(elementsMaxX, viewportMaxX);
const sceneMaxY = Math.max(elementsMaxY, viewportMaxY);
return {
horizontal: viewportMinX === sceneMinX && viewportMaxX === sceneMaxX ? null : {
x: Math.max(safeArea.left, SCROLLBAR_MARGIN) + (viewportMinX - sceneMinX) / (sceneMaxX - sceneMinX) * viewportWidth,
y: viewportHeight - SCROLLBAR_WIDTH - Math.max(SCROLLBAR_MARGIN, safeArea.bottom),
width: (viewportMaxX - viewportMinX) / (sceneMaxX - sceneMinX) * viewportWidth - Math.max(SCROLLBAR_MARGIN * 2, safeArea.left + safeArea.right),
height: SCROLLBAR_WIDTH
},
vertical: viewportMinY === sceneMinY && viewportMaxY === sceneMaxY ? null : {
x: isRTL ? Math.max(safeArea.left, SCROLLBAR_MARGIN) : viewportWidth - SCROLLBAR_WIDTH - Math.max(safeArea.right, SCROLLBAR_MARGIN),
y: (viewportMinY - sceneMinY) / (sceneMaxY - sceneMinY) * viewportHeight + Math.max(safeArea.top, SCROLLBAR_MARGIN),
width: SCROLLBAR_WIDTH,
height: (viewportMaxY - viewportMinY) / (sceneMaxY - sceneMinY) * viewportHeight - Math.max(SCROLLBAR_MARGIN * 2, safeArea.top + safeArea.bottom)
}
};
};
var isOverScrollBars = (scrollBars, x, y) => {
const [isOverHorizontal, isOverVertical] = [
scrollBars.horizontal,
scrollBars.vertical
].map((scrollBar) => {
return scrollBar != null && scrollBar.x <= x && x <= scrollBar.x + scrollBar.width && scrollBar.y <= y && y <= scrollBar.y + scrollBar.height;
});
const isOverEither = isOverHorizontal || isOverVertical;
return { isOverEither, isOverHorizontal, isOverVertical };
};
// components/PenModeButton.tsx
import clsx39 from "clsx";
import { jsx as jsx88, jsxs as jsxs48 } from "react/jsx-runtime";
var DEFAULT_SIZE2 = "medium";
var PenModeButton = (props) => {
if (!props.penDetected) {
return null;
}
return /* @__PURE__ */ jsxs48(
"label",
{
className: clsx39(
"ToolIcon ToolIcon__penMode",
`ToolIcon_size_${DEFAULT_SIZE2}`,
{
"is-mobile": props.isMobile
}
),
title: `${props.title}`,
children: [
/* @__PURE__ */ jsx88(
"input",
{
className: "ToolIcon_type_checkbox",
type: "checkbox",
name: props.name,
onChange: props.onChange,
checked: props.checked,
"aria-label": props.title
}
),
/* @__PURE__ */ jsx88("div", { className: "ToolIcon__icon", children: PenModeIcon })
]
}
);
};
// components/HandButton.tsx
import clsx40 from "clsx";
import { jsx as jsx89 } from "react/jsx-runtime";
var HandButton = (props) => {
return /* @__PURE__ */ jsx89(
ToolButton,
{
className: clsx40("Shape", { fillable: false }),
type: "radio",
icon: handIcon,
name: "editor-current-shape",
checked: props.checked,
title: `${props.title} \u2014 H`,
keyBindingLabel: !props.isMobile ? KEYS.H.toLocaleUpperCase() : void 0,
"aria-label": `${props.title} \u2014 H`,
"aria-keyshortcuts": KEYS.H,
"data-testid": `toolbar-hand`,
onChange: () => props.onChange?.()
}
);
};
// components/MobileMenu.tsx
import { Fragment as Fragment12, jsx as jsx90, jsxs as jsxs49 } from "react/jsx-runtime";
var MobileMenu = ({
appState,
elements,
actionManager,
setAppState,
onLockToggle,
onHandToolToggle,
onPenModeToggle,
renderTopRightUI,
renderCustomStats,
renderSidebars,
device,
renderWelcomeScreen,
UIOptions,
app
}) => {
const {
WelcomeScreenCenterTunnel,
MainMenuTunnel,
DefaultSidebarTriggerTunnel
} = useTunnels();
const renderToolbar = () => {
return /* @__PURE__ */ jsxs49(FixedSideContainer, { side: "top", className: "App-top-bar", children: [
renderWelcomeScreen && /* @__PURE__ */ jsx90(WelcomeScreenCenterTunnel.Out, {}),
/* @__PURE__ */ jsx90(Section, { heading: "shapes", children: (heading) => /* @__PURE__ */ jsx90(Stack_default.Col, { gap: 4, align: "center", children: /* @__PURE__ */ jsxs49(Stack_default.Row, { gap: 1, className: "App-toolbar-container", children: [
/* @__PURE__ */ jsxs49(Island, { padding: 1, className: "App-toolbar App-toolbar--mobile", children: [
heading,
/* @__PURE__ */ jsx90(Stack_default.Row, { gap: 1, children: /* @__PURE__ */ jsx90(
ShapesSwitcher,
{
appState,
activeTool: appState.activeTool,
UIOptions,
app
}
) })
] }),
renderTopRightUI && renderTopRightUI(true, appState),
/* @__PURE__ */ jsxs49("div", { className: "mobile-misc-tools-container", children: [
!appState.viewModeEnabled && appState.openDialog?.name !== "elementLinkSelector" && /* @__PURE__ */ jsx90(DefaultSidebarTriggerTunnel.Out, {}),
/* @__PURE__ */ jsx90(
PenModeButton,
{
checked: appState.penMode,
onChange: () => onPenModeToggle(null),
title: t("toolBar.penMode"),
isMobile: true,
penDetected: appState.penDetected
}
),
/* @__PURE__ */ jsx90(
LockButton,
{
checked: appState.activeTool.locked,
onChange: onLockToggle,
title: t("toolBar.lock"),
isMobile: true
}
),
/* @__PURE__ */ jsx90(
HandButton,
{
checked: isHandToolActive(appState),
onChange: () => onHandToolToggle(),
title: t("toolBar.hand"),
isMobile: true
}
)
] })
] }) }) }),
/* @__PURE__ */ jsx90(
HintViewer,
{
appState,
isMobile: true,
device,
app
}
)
] });
};
const renderAppToolbar = () => {
if (appState.viewModeEnabled || appState.openDialog?.name === "elementLinkSelector") {
return /* @__PURE__ */ jsx90("div", { className: "App-toolbar-content", children: /* @__PURE__ */ jsx90(MainMenuTunnel.Out, {}) });
}
return /* @__PURE__ */ jsxs49("div", { className: "App-toolbar-content", children: [
/* @__PURE__ */ jsx90(MainMenuTunnel.Out, {}),
actionManager.renderAction("toggleEditMenu"),
actionManager.renderAction(
appState.multiElement ? "finalize" : "duplicateSelection"
),
actionManager.renderAction("deleteSelectedElements"),
/* @__PURE__ */ jsxs49("div", { children: [
actionManager.renderAction("undo"),
actionManager.renderAction("redo")
] })
] });
};
return /* @__PURE__ */ jsxs49(Fragment12, { children: [
renderSidebars(),
!appState.viewModeEnabled && appState.openDialog?.name !== "elementLinkSelector" && renderToolbar(),
/* @__PURE__ */ jsx90(
"div",
{
className: "App-bottom-bar",
style: {
marginBottom: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginLeft: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2,
marginRight: SCROLLBAR_WIDTH + SCROLLBAR_MARGIN * 2
},
children: /* @__PURE__ */ jsxs49(Island, { padding: 0, children: [
appState.openMenu === "shape" && !appState.viewModeEnabled && appState.openDialog?.name !== "elementLinkSelector" && showSelectedShapeActions(appState, elements) ? /* @__PURE__ */ jsx90(Section, { className: "App-mobile-menu", heading: "selectedShapeActions", children: /* @__PURE__ */ jsx90(
SelectedShapeActions,
{
appState,
elementsMap: app.scene.getNonDeletedElementsMap(),
renderAction: actionManager.renderAction,
app
}
) }) : null,
/* @__PURE__ */ jsxs49("footer", { className: "App-toolbar", children: [
renderAppToolbar(),
appState.scrolledOutside && !appState.openMenu && !appState.openSidebar && /* @__PURE__ */ jsx90(
"button",
{
type: "button",
className: "scroll-back-to-content",
onClick: () => {
setAppState((appState2) => ({
...calculateScrollCenter(elements, appState2)
}));
},
children: t("buttons.scrollBackToContent")
}
)
] })
] })
}
)
] });
};
// components/PasteChartDialog.tsx
import oc from "open-color";
import React33, { useLayoutEffect as useLayoutEffect5, useRef as useRef26, useState as useState29 } from "react";
import { jsx as jsx91, jsxs as jsxs50 } from "react/jsx-runtime";
var ChartPreviewBtn = (props) => {
const previewRef = useRef26(null);
const [chartElements, setChartElements] = useState29(
null
);
useLayoutEffect5(() => {
if (!props.spreadsheet) {
return;
}
const elements = renderSpreadsheet(
props.chartType,
props.spreadsheet,
0,
0
);
setChartElements(elements);
let svg;
const previewNode = previewRef.current;
(async () => {
svg = await exportToSvg(
elements,
{
exportBackground: false,
viewBackgroundColor: oc.white
},
null,
// files
{
skipInliningFonts: true
}
);
svg.querySelector(".style-fonts")?.remove();
previewNode.replaceChildren();
previewNode.appendChild(svg);
if (props.selected) {
previewNode.parentNode.focus();
}
})();
return () => {
previewNode.replaceChildren();
};
}, [props.spreadsheet, props.chartType, props.selected]);
return /* @__PURE__ */ jsx91(
"button",
{
type: "button",
className: "ChartPreview",
onClick: () => {
if (chartElements) {
props.onClick(props.chartType, chartElements);
}
},
children: /* @__PURE__ */ jsx91("div", { ref: previewRef })
}
);
};
var PasteChartDialog = ({
setAppState,
appState,
onClose
}) => {
const { onInsertElements } = useApp();
const handleClose = React33.useCallback(() => {
if (onClose) {
onClose();
}
}, [onClose]);
const handleChartClick = (chartType, elements) => {
onInsertElements(elements);
trackEvent("paste", "chart", chartType);
setAppState({
currentChartType: chartType,
pasteDialog: {
shown: false,
data: null
}
});
};
return /* @__PURE__ */ jsx91(
Dialog,
{
size: "small",
onCloseRequest: handleClose,
title: t("labels.pasteCharts"),
className: "PasteChartDialog",
autofocus: false,
children: /* @__PURE__ */ jsxs50("div", { className: "container", children: [
/* @__PURE__ */ jsx91(
ChartPreviewBtn,
{
chartType: "bar",
spreadsheet: appState.pasteDialog.data,
selected: appState.currentChartType === "bar",
onClick: handleChartClick
}
),
/* @__PURE__ */ jsx91(
ChartPreviewBtn,
{
chartType: "line",
spreadsheet: appState.pasteDialog.data,
selected: appState.currentChartType === "line",
onClick: handleChartClick
}
)
] })
}
);
};
// components/HelpDialog.tsx
import React34 from "react";
import { Fragment as Fragment13, jsx as jsx92, jsxs as jsxs51 } from "react/jsx-runtime";
var Header = () => /* @__PURE__ */ jsxs51("div", { className: "HelpDialog__header", children: [
/* @__PURE__ */ jsxs51(
"a",
{
className: "HelpDialog__btn",
href: "https://docs.excalidraw.com",
target: "_blank",
rel: "noopener noreferrer",
children: [
/* @__PURE__ */ jsx92("div", { className: "HelpDialog__link-icon", children: ExternalLinkIcon }),
t("helpDialog.documentation")
]
}
),
/* @__PURE__ */ jsxs51(
"a",
{
className: "HelpDialog__btn",
href: "https://plus.excalidraw.com/blog",
target: "_blank",
rel: "noopener noreferrer",
children: [
/* @__PURE__ */ jsx92("div", { className: "HelpDialog__link-icon", children: ExternalLinkIcon }),
t("helpDialog.blog")
]
}
),
/* @__PURE__ */ jsxs51(
"a",
{
className: "HelpDialog__btn",
href: "https://github.com/excalidraw/excalidraw/issues",
target: "_blank",
rel: "noopener noreferrer",
children: [
/* @__PURE__ */ jsx92("div", { className: "HelpDialog__link-icon", children: GithubIcon }),
t("helpDialog.github")
]
}
),
/* @__PURE__ */ jsxs51(
"a",
{
className: "HelpDialog__btn",
href: "https://youtube.com/@excalidraw",
target: "_blank",
rel: "noopener noreferrer",
children: [
/* @__PURE__ */ jsx92("div", { className: "HelpDialog__link-icon", children: youtubeIcon }),
"YouTube"
]
}
)
] });
var Section2 = (props) => /* @__PURE__ */ jsxs51(Fragment13, { children: [
/* @__PURE__ */ jsx92("h3", { children: props.title }),
/* @__PURE__ */ jsx92("div", { className: "HelpDialog__islands-container", children: props.children })
] });
var ShortcutIsland = (props) => /* @__PURE__ */ jsxs51("div", { className: `HelpDialog__island ${props.className}`, children: [
/* @__PURE__ */ jsx92("h4", { className: "HelpDialog__island-title", children: props.caption }),
/* @__PURE__ */ jsx92("div", { className: "HelpDialog__island-content", children: props.children })
] });
function* intersperse(as, delim) {
let first = true;
for (const x of as) {
if (!first) {
yield delim;
}
first = false;
yield x;
}
}
var upperCaseSingleChars = (str) => {
return str.replace(/\b[a-z]\b/, (c) => c.toUpperCase());
};
var Shortcut = ({
label,
shortcuts,
isOr = true
}) => {
const splitShortcutKeys = shortcuts.map((shortcut) => {
const keys = shortcut.endsWith("++") ? [...shortcut.slice(0, -2).split("+"), "+"] : shortcut.split("+");
return keys.map((key) => /* @__PURE__ */ jsx92(ShortcutKey, { children: upperCaseSingleChars(key) }, key));
});
return /* @__PURE__ */ jsxs51("div", { className: "HelpDialog__shortcut", children: [
/* @__PURE__ */ jsx92("div", { children: label }),
/* @__PURE__ */ jsx92("div", { className: "HelpDialog__key-container", children: [...intersperse(splitShortcutKeys, isOr ? t("helpDialog.or") : null)] })
] });
};
var ShortcutKey = (props) => /* @__PURE__ */ jsx92("kbd", { className: "HelpDialog__key", ...props });
var HelpDialog = ({ onClose }) => {
const handleClose = React34.useCallback(() => {
if (onClose) {
onClose();
}
}, [onClose]);
return /* @__PURE__ */ jsx92(Fragment13, { children: /* @__PURE__ */ jsxs51(
Dialog,
{
onCloseRequest: handleClose,
title: t("helpDialog.title"),
className: "HelpDialog",
children: [
/* @__PURE__ */ jsx92(Header, {}),
/* @__PURE__ */ jsxs51(Section2, { title: t("helpDialog.shortcuts"), children: [
/* @__PURE__ */ jsxs51(
ShortcutIsland,
{
className: "HelpDialog__island--tools",
caption: t("helpDialog.tools"),
children: [
/* @__PURE__ */ jsx92(Shortcut, { label: t("toolBar.hand"), shortcuts: [KEYS.H] }),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("toolBar.selection"),
shortcuts: [KEYS.V, KEYS["1"]]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("toolBar.rectangle"),
shortcuts: [KEYS.R, KEYS["2"]]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("toolBar.diamond"),
shortcuts: [KEYS.D, KEYS["3"]]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("toolBar.ellipse"),
shortcuts: [KEYS.O, KEYS["4"]]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("toolBar.arrow"),
shortcuts: [KEYS.A, KEYS["5"]]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("toolBar.line"),
shortcuts: [KEYS.L, KEYS["6"]]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("toolBar.freedraw"),
shortcuts: [KEYS.P, KEYS["7"]]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("toolBar.text"),
shortcuts: [KEYS.T, KEYS["8"]]
}
),
/* @__PURE__ */ jsx92(Shortcut, { label: t("toolBar.image"), shortcuts: [KEYS["9"]] }),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("toolBar.eraser"),
shortcuts: [KEYS.E, KEYS["0"]]
}
),
/* @__PURE__ */ jsx92(Shortcut, { label: t("toolBar.frame"), shortcuts: [KEYS.F] }),
/* @__PURE__ */ jsx92(Shortcut, { label: t("toolBar.laser"), shortcuts: [KEYS.K] }),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.eyeDropper"),
shortcuts: [KEYS.I, "Shift+S", "Shift+G"]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.editLineArrowPoints"),
shortcuts: [getShortcutKey("CtrlOrCmd+Enter")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.editText"),
shortcuts: [getShortcutKey("Enter")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.textNewLine"),
shortcuts: [
getShortcutKey("Enter"),
getShortcutKey("Shift+Enter")
]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.textFinish"),
shortcuts: [
getShortcutKey("Esc"),
getShortcutKey("CtrlOrCmd+Enter")
]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.curvedArrow"),
shortcuts: [
"A",
t("helpDialog.click"),
t("helpDialog.click"),
t("helpDialog.click")
],
isOr: false
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.curvedLine"),
shortcuts: [
"L",
t("helpDialog.click"),
t("helpDialog.click"),
t("helpDialog.click")
],
isOr: false
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.cropStart"),
shortcuts: [t("helpDialog.doubleClick"), getShortcutKey("Enter")],
isOr: true
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.cropFinish"),
shortcuts: [getShortcutKey("Enter"), getShortcutKey("Escape")],
isOr: true
}
),
/* @__PURE__ */ jsx92(Shortcut, { label: t("toolBar.lock"), shortcuts: [KEYS.Q] }),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.preventBinding"),
shortcuts: [getShortcutKey("CtrlOrCmd")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("toolBar.link"),
shortcuts: [getShortcutKey("CtrlOrCmd+K")]
}
)
]
}
),
/* @__PURE__ */ jsxs51(
ShortcutIsland,
{
className: "HelpDialog__island--view",
caption: t("helpDialog.view"),
children: [
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("buttons.zoomIn"),
shortcuts: [getShortcutKey("CtrlOrCmd++")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("buttons.zoomOut"),
shortcuts: [getShortcutKey("CtrlOrCmd+-")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("buttons.resetZoom"),
shortcuts: [getShortcutKey("CtrlOrCmd+0")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.zoomToFit"),
shortcuts: ["Shift+1"]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.zoomToSelection"),
shortcuts: ["Shift+2"]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.movePageUpDown"),
shortcuts: ["PgUp/PgDn"]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.movePageLeftRight"),
shortcuts: ["Shift+PgUp/PgDn"]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("buttons.zenMode"),
shortcuts: [getShortcutKey("Alt+Z")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("buttons.objectsSnapMode"),
shortcuts: [getShortcutKey("Alt+S")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.toggleGrid"),
shortcuts: [getShortcutKey("CtrlOrCmd+'")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.viewMode"),
shortcuts: [getShortcutKey("Alt+R")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.toggleTheme"),
shortcuts: [getShortcutKey("Alt+Shift+D")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("stats.fullTitle"),
shortcuts: [getShortcutKey("Alt+/")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("search.title"),
shortcuts: [getShortcutFromShortcutName("searchMenu")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("commandPalette.title"),
shortcuts: isFirefox ? [getShortcutFromShortcutName("commandPalette")] : [
getShortcutFromShortcutName("commandPalette"),
getShortcutFromShortcutName("commandPalette", 1)
]
}
)
]
}
),
/* @__PURE__ */ jsxs51(
ShortcutIsland,
{
className: "HelpDialog__island--editor",
caption: t("helpDialog.editor"),
children: [
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.createFlowchart"),
shortcuts: [getShortcutKey(`CtrlOrCmd+Arrow Key`)],
isOr: true
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.navigateFlowchart"),
shortcuts: [getShortcutKey(`Alt+Arrow Key`)],
isOr: true
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.moveCanvas"),
shortcuts: [
getShortcutKey(`Space+${t("helpDialog.drag")}`),
getShortcutKey(`Wheel+${t("helpDialog.drag")}`)
],
isOr: true
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("buttons.clearReset"),
shortcuts: [getShortcutKey("CtrlOrCmd+Delete")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.delete"),
shortcuts: [getShortcutKey("Delete")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.cut"),
shortcuts: [getShortcutKey("CtrlOrCmd+X")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.copy"),
shortcuts: [getShortcutKey("CtrlOrCmd+C")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.paste"),
shortcuts: [getShortcutKey("CtrlOrCmd+V")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.pasteAsPlaintext"),
shortcuts: [getShortcutKey("CtrlOrCmd+Shift+V")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.selectAll"),
shortcuts: [getShortcutKey("CtrlOrCmd+A")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.multiSelect"),
shortcuts: [getShortcutKey(`Shift+${t("helpDialog.click")}`)]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.deepSelect"),
shortcuts: [getShortcutKey(`CtrlOrCmd+${t("helpDialog.click")}`)]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.deepBoxSelect"),
shortcuts: [getShortcutKey(`CtrlOrCmd+${t("helpDialog.drag")}`)]
}
),
(probablySupportsClipboardBlob || isFirefox) && /* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.copyAsPng"),
shortcuts: [getShortcutKey("Shift+Alt+C")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.copyStyles"),
shortcuts: [getShortcutKey("CtrlOrCmd+Alt+C")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.pasteStyles"),
shortcuts: [getShortcutKey("CtrlOrCmd+Alt+V")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.sendToBack"),
shortcuts: [
isDarwin ? getShortcutKey("CtrlOrCmd+Alt+[") : getShortcutKey("CtrlOrCmd+Shift+[")
]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.bringToFront"),
shortcuts: [
isDarwin ? getShortcutKey("CtrlOrCmd+Alt+]") : getShortcutKey("CtrlOrCmd+Shift+]")
]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.sendBackward"),
shortcuts: [getShortcutKey("CtrlOrCmd+[")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.bringForward"),
shortcuts: [getShortcutKey("CtrlOrCmd+]")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.alignTop"),
shortcuts: [getShortcutKey("CtrlOrCmd+Shift+Up")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.alignBottom"),
shortcuts: [getShortcutKey("CtrlOrCmd+Shift+Down")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.alignLeft"),
shortcuts: [getShortcutKey("CtrlOrCmd+Shift+Left")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.alignRight"),
shortcuts: [getShortcutKey("CtrlOrCmd+Shift+Right")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.duplicateSelection"),
shortcuts: [
getShortcutKey("CtrlOrCmd+D"),
getShortcutKey(`Alt+${t("helpDialog.drag")}`)
]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("helpDialog.toggleElementLock"),
shortcuts: [getShortcutKey("CtrlOrCmd+Shift+L")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("buttons.undo"),
shortcuts: [getShortcutKey("CtrlOrCmd+Z")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("buttons.redo"),
shortcuts: isWindows ? [
getShortcutKey("CtrlOrCmd+Y"),
getShortcutKey("CtrlOrCmd+Shift+Z")
] : [getShortcutKey("CtrlOrCmd+Shift+Z")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.group"),
shortcuts: [getShortcutKey("CtrlOrCmd+G")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.ungroup"),
shortcuts: [getShortcutKey("CtrlOrCmd+Shift+G")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.flipHorizontal"),
shortcuts: [getShortcutKey("Shift+H")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.flipVertical"),
shortcuts: [getShortcutKey("Shift+V")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.showStroke"),
shortcuts: [getShortcutKey("S")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.showBackground"),
shortcuts: [getShortcutKey("G")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.showFonts"),
shortcuts: [getShortcutKey("Shift+F")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.decreaseFontSize"),
shortcuts: [getShortcutKey("CtrlOrCmd+Shift+<")]
}
),
/* @__PURE__ */ jsx92(
Shortcut,
{
label: t("labels.increaseFontSize"),
shortcuts: [getShortcutKey("CtrlOrCmd+Shift+>")]
}
)
]
}
)
] })
]
}
) });
};
// components/UserList.tsx
import React35, { useLayoutEffect as useLayoutEffect6 } from "react";
import clsx41 from "clsx";
import * as Popover7 from "@radix-ui/react-popover";
import { Fragment as Fragment14, jsx as jsx93, jsxs as jsxs52 } from "react/jsx-runtime";
var DEFAULT_MAX_AVATARS = 4;
var SHOW_COLLABORATORS_FILTER_AT = 8;
var ConditionalTooltipWrapper = ({
shouldWrap,
children,
username
}) => shouldWrap ? /* @__PURE__ */ jsx93(Tooltip, { label: username || "Unknown user", children }) : /* @__PURE__ */ jsx93(Fragment14, { children });
var renderCollaborator = ({
actionManager,
collaborator,
socketId,
withName = false,
shouldWrapWithTooltip = false,
isBeingFollowed
}) => {
const data = {
socketId,
collaborator,
withName,
isBeingFollowed
};
const avatarJSX = actionManager.renderAction("goToCollaborator", data);
return /* @__PURE__ */ jsx93(
ConditionalTooltipWrapper,
{
username: collaborator.username,
shouldWrap: shouldWrapWithTooltip,
children: avatarJSX
},
socketId
);
};
var collaboratorComparatorKeys = [
"avatarUrl",
"id",
"socketId",
"username",
"isInCall",
"isSpeaking",
"isMuted"
];
var UserList = React35.memo(
({ className, mobile, collaborators, userToFollow }) => {
const actionManager = useExcalidrawActionManager();
const uniqueCollaboratorsMap = /* @__PURE__ */ new Map();
collaborators.forEach((collaborator, socketId) => {
const userId = collaborator.id || socketId;
uniqueCollaboratorsMap.set(
// filter on user id, else fall back on unique socketId
userId,
{ ...collaborator, socketId }
);
});
const uniqueCollaboratorsArray = Array.from(
uniqueCollaboratorsMap.values()
).filter((collaborator) => collaborator.username?.trim());
const [searchTerm, setSearchTerm] = React35.useState("");
const filteredCollaborators = uniqueCollaboratorsArray.filter(
(collaborator) => collaborator.username?.toLowerCase().includes(searchTerm)
);
const userListWrapper = React35.useRef(null);
useLayoutEffect6(() => {
if (userListWrapper.current) {
const updateMaxAvatars = (width) => {
const maxAvatars2 = Math.max(1, Math.min(8, Math.floor(width / 38)));
setMaxAvatars(maxAvatars2);
};
updateMaxAvatars(userListWrapper.current.clientWidth);
if (!supportsResizeObserver) {
return;
}
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width } = entry.contentRect;
updateMaxAvatars(width);
}
});
resizeObserver.observe(userListWrapper.current);
return () => {
resizeObserver.disconnect();
};
}
}, []);
const [maxAvatars, setMaxAvatars] = React35.useState(DEFAULT_MAX_AVATARS);
const firstNCollaborators = uniqueCollaboratorsArray.slice(
0,
maxAvatars - 1
);
const firstNAvatarsJSX = firstNCollaborators.map(
(collaborator) => renderCollaborator({
actionManager,
collaborator,
socketId: collaborator.socketId,
shouldWrapWithTooltip: true,
isBeingFollowed: collaborator.socketId === userToFollow
})
);
return mobile ? /* @__PURE__ */ jsx93("div", { className: clsx41("UserList UserList_mobile", className), children: uniqueCollaboratorsArray.map(
(collaborator) => renderCollaborator({
actionManager,
collaborator,
socketId: collaborator.socketId,
shouldWrapWithTooltip: true,
isBeingFollowed: collaborator.socketId === userToFollow
})
) }) : /* @__PURE__ */ jsx93("div", { className: "UserList__wrapper", ref: userListWrapper, children: /* @__PURE__ */ jsxs52(
"div",
{
className: clsx41("UserList", className),
style: { [`--max-avatars`]: maxAvatars },
children: [
firstNAvatarsJSX,
uniqueCollaboratorsArray.length > maxAvatars - 1 && /* @__PURE__ */ jsxs52(Popover7.Root, { children: [
/* @__PURE__ */ jsxs52(Popover7.Trigger, { className: "UserList__more", children: [
"+",
uniqueCollaboratorsArray.length - maxAvatars + 1
] }),
/* @__PURE__ */ jsx93(
Popover7.Content,
{
style: {
zIndex: 2,
width: "15rem",
textAlign: "left"
},
align: "end",
sideOffset: 10,
children: /* @__PURE__ */ jsxs52(Island, { padding: 2, children: [
uniqueCollaboratorsArray.length >= SHOW_COLLABORATORS_FILTER_AT && /* @__PURE__ */ jsx93(
QuickSearch,
{
placeholder: t("quickSearch.placeholder"),
onChange: setSearchTerm
}
),
/* @__PURE__ */ jsx93(
ScrollableList,
{
className: "dropdown-menu UserList__collaborators",
placeholder: t("userList.empty"),
children: filteredCollaborators.length > 0 ? [
/* @__PURE__ */ jsx93("div", { className: "hint", children: t("userList.hint.text") }),
filteredCollaborators.map(
(collaborator) => renderCollaborator({
actionManager,
collaborator,
socketId: collaborator.socketId,
withName: true,
isBeingFollowed: collaborator.socketId === userToFollow
})
)
] : []
}
),
/* @__PURE__ */ jsx93(
Popover7.Arrow,
{
width: 20,
height: 10,
style: {
fill: "var(--popup-bg-color)",
filter: "drop-shadow(rgba(0, 0, 0, 0.05) 0px 3px 2px)"
}
}
)
] })
}
)
] })
]
}
) });
},
(prev, next) => {
if (prev.collaborators.size !== next.collaborators.size || prev.mobile !== next.mobile || prev.className !== next.className || prev.userToFollow !== next.userToFollow) {
return false;
}
const nextCollaboratorSocketIds = next.collaborators.keys();
for (const [socketId, collaborator] of prev.collaborators) {
const nextCollaborator = next.collaborators.get(socketId);
if (!nextCollaborator || // this checks order of collaborators in the map is the same
// as previous render
socketId !== nextCollaboratorSocketIds.next().value || !isShallowEqual(
collaborator,
nextCollaborator,
collaboratorComparatorKeys
)) {
return false;
}
}
return true;
}
);
// components/JSONExportDialog.tsx
import React36 from "react";
// components/Card.tsx
import OpenColor2 from "open-color";
import { jsx as jsx94 } from "react/jsx-runtime";
var Card = ({ children, color }) => {
return /* @__PURE__ */ jsx94(
"div",
{
className: "Card",
style: {
["--card-color"]: color === "primary" ? "var(--color-primary)" : OpenColor2[color][7],
["--card-color-darker"]: color === "primary" ? "var(--color-primary-darker)" : OpenColor2[color][8],
["--card-color-darkest"]: color === "primary" ? "var(--color-primary-darkest)" : OpenColor2[color][9]
},
children
}
);
};
// components/JSONExportDialog.tsx
import { Fragment as Fragment15, jsx as jsx95, jsxs as jsxs53 } from "react/jsx-runtime";
var JSONExportModal = ({
elements,
appState,
setAppState,
files,
actionManager,
exportOpts,
canvas,
onCloseRequest
}) => {
const { onExportToBackend } = exportOpts;
return /* @__PURE__ */ jsx95("div", { className: "ExportDialog ExportDialog--json", children: /* @__PURE__ */ jsxs53("div", { className: "ExportDialog-cards", children: [
exportOpts.saveFileToDisk && /* @__PURE__ */ jsxs53(Card, { color: "lime", children: [
/* @__PURE__ */ jsx95("div", { className: "Card-icon", children: exportToFileIcon }),
/* @__PURE__ */ jsx95("h2", { children: t("exportDialog.disk_title") }),
/* @__PURE__ */ jsxs53("div", { className: "Card-details", children: [
t("exportDialog.disk_details"),
!nativeFileSystemSupported && actionManager.renderAction("changeProjectName")
] }),
/* @__PURE__ */ jsx95(
ToolButton,
{
className: "Card-button",
type: "button",
title: t("exportDialog.disk_button"),
"aria-label": t("exportDialog.disk_button"),
showAriaLabel: true,
onClick: () => {
actionManager.executeAction(actionSaveFileToDisk, "ui");
}
}
)
] }),
onExportToBackend && /* @__PURE__ */ jsxs53(Card, { color: "pink", children: [
/* @__PURE__ */ jsx95("div", { className: "Card-icon", children: LinkIcon }),
/* @__PURE__ */ jsx95("h2", { children: t("exportDialog.link_title") }),
/* @__PURE__ */ jsx95("div", { className: "Card-details", children: t("exportDialog.link_details") }),
/* @__PURE__ */ jsx95(
ToolButton,
{
className: "Card-button",
type: "button",
title: t("exportDialog.link_button"),
"aria-label": t("exportDialog.link_button"),
showAriaLabel: true,
onClick: async () => {
try {
trackEvent("export", "link", `ui (${getFrame()})`);
await onExportToBackend(elements, appState, files);
onCloseRequest();
} catch (error) {
setAppState({ errorMessage: error.message });
}
}
}
)
] }),
exportOpts.renderCustomUI && exportOpts.renderCustomUI(elements, appState, files, canvas)
] }) });
};
var JSONExportDialog = ({
elements,
appState,
files,
actionManager,
exportOpts,
canvas,
setAppState
}) => {
const handleClose = React36.useCallback(() => {
setAppState({ openDialog: null });
}, [setAppState]);
return /* @__PURE__ */ jsx95(Fragment15, { children: appState.openDialog?.name === "jsonExport" && /* @__PURE__ */ jsx95(Dialog, { onCloseRequest: handleClose, title: t("buttons.export"), children: /* @__PURE__ */ jsx95(
JSONExportModal,
{
elements,
appState,
setAppState,
files,
actionManager,
onCloseRequest: handleClose,
exportOpts,
canvas
}
) }) });
};
// components/footer/Footer.tsx
import clsx42 from "clsx";
// components/HelpButton.tsx
import { jsx as jsx96 } from "react/jsx-runtime";
var HelpButton = (props) => /* @__PURE__ */ jsx96(
"button",
{
className: "help-icon",
onClick: props.onClick,
type: "button",
title: `${t("helpDialog.title")} \u2014 ?`,
"aria-label": t("helpDialog.title"),
children: HelpIcon
}
);
// components/footer/Footer.tsx
import { jsx as jsx97, jsxs as jsxs54 } from "react/jsx-runtime";
var Footer = ({
appState,
actionManager,
showExitZenModeBtn,
renderWelcomeScreen
}) => {
const { FooterCenterTunnel, WelcomeScreenHelpHintTunnel } = useTunnels();
const device = useDevice();
const showFinalize = !appState.viewModeEnabled && appState.multiElement && device.isTouchScreen;
return /* @__PURE__ */ jsxs54(
"footer",
{
role: "contentinfo",
className: "layer-ui__wrapper__footer App-menu App-menu_bottom",
children: [
/* @__PURE__ */ jsx97(
"div",
{
className: clsx42("layer-ui__wrapper__footer-left zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left": appState.zenModeEnabled
}),
children: /* @__PURE__ */ jsx97(Stack_default.Col, { gap: 2, children: /* @__PURE__ */ jsxs54(Section, { heading: "canvasActions", children: [
/* @__PURE__ */ jsx97(
ZoomActions,
{
renderAction: actionManager.renderAction,
zoom: appState.zoom
}
),
!appState.viewModeEnabled && /* @__PURE__ */ jsx97(
UndoRedoActions,
{
renderAction: actionManager.renderAction,
className: clsx42("zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom": appState.zenModeEnabled
})
}
),
showFinalize && /* @__PURE__ */ jsx97(
FinalizeAction,
{
renderAction: actionManager.renderAction,
className: clsx42("zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-left": appState.zenModeEnabled
})
}
)
] }) })
}
),
/* @__PURE__ */ jsx97(FooterCenterTunnel.Out, {}),
/* @__PURE__ */ jsx97(
"div",
{
className: clsx42("layer-ui__wrapper__footer-right zen-mode-transition", {
"transition-right": appState.zenModeEnabled
}),
children: /* @__PURE__ */ jsxs54("div", { style: { position: "relative" }, children: [
renderWelcomeScreen && /* @__PURE__ */ jsx97(WelcomeScreenHelpHintTunnel.Out, {}),
/* @__PURE__ */ jsx97(
HelpButton,
{
onClick: () => actionManager.executeAction(actionShortcuts)
}
)
] })
}
),
/* @__PURE__ */ jsx97(
ExitZenModeAction,
{
actionManager,
showExitZenModeBtn
}
)
]
}
);
};
var Footer_default = Footer;
Footer.displayName = "Footer";
// components/Sidebar/Sidebar.tsx
import {
useEffect as useEffect32,
useLayoutEffect as useLayoutEffect7,
useRef as useRef27,
useState as useState30,
forwardRef as forwardRef5,
useImperativeHandle as useImperativeHandle2,
useCallback as useCallback12
} from "react";
// components/Sidebar/common.ts
import React37 from "react";
var SidebarPropsContext = React37.createContext({});
// components/Sidebar/SidebarHeader.tsx
import clsx43 from "clsx";
import { useContext as useContext2 } from "react";
import { jsx as jsx98, jsxs as jsxs55 } from "react/jsx-runtime";
var SidebarHeader = ({
children,
className
}) => {
const device = useDevice();
const props = useContext2(SidebarPropsContext);
const renderDockButton = !!(device.editor.canFitSidebar && props.shouldRenderDockButton);
return /* @__PURE__ */ jsxs55(
"div",
{
className: clsx43("sidebar__header", className),
"data-testid": "sidebar-header",
children: [
children,
/* @__PURE__ */ jsxs55("div", { className: "sidebar__header__buttons", children: [
renderDockButton && /* @__PURE__ */ jsx98(Tooltip, { label: t("labels.sidebarLock"), children: /* @__PURE__ */ jsx98(
Button,
{
onSelect: () => props.onDock?.(!props.docked),
selected: !!props.docked,
className: "sidebar__dock",
"data-testid": "sidebar-dock",
"aria-label": t("labels.sidebarLock"),
children: PinIcon
}
) }),
/* @__PURE__ */ jsx98(
Button,
{
"data-testid": "sidebar-close",
className: "sidebar__close",
onSelect: props.onCloseRequest,
"aria-label": t("buttons.close"),
children: CloseIcon
}
)
] })
]
}
);
};
SidebarHeader.displayName = "SidebarHeader";
// components/Sidebar/Sidebar.tsx
import clsx45 from "clsx";
// components/Sidebar/SidebarTrigger.tsx
import clsx44 from "clsx";
import { jsx as jsx99, jsxs as jsxs56 } from "react/jsx-runtime";
var SidebarTrigger = ({
name,
tab,
icon,
title,
children,
onToggle,
className,
style
}) => {
const setAppState = useExcalidrawSetAppState();
const appState = useUIAppState();
return /* @__PURE__ */ jsxs56("label", { title, className: "sidebar-trigger__label-element", children: [
/* @__PURE__ */ jsx99(
"input",
{
className: "ToolIcon_type_checkbox",
type: "checkbox",
onChange: (event) => {
document.querySelector(".layer-ui__wrapper")?.classList.remove("animate");
const isOpen = event.target.checked;
setAppState({ openSidebar: isOpen ? { name, tab } : null });
onToggle?.(isOpen);
},
checked: appState.openSidebar?.name === name,
"aria-label": title,
"aria-keyshortcuts": "0"
}
),
/* @__PURE__ */ jsxs56("div", { className: clsx44("sidebar-trigger", className), style, children: [
icon && /* @__PURE__ */ jsx99("div", { children: icon }),
children && /* @__PURE__ */ jsx99("div", { className: "sidebar-trigger__label", children })
] })
] });
};
SidebarTrigger.displayName = "SidebarTrigger";
// components/Sidebar/SidebarTabTriggers.tsx
import * as RadixTabs from "@radix-ui/react-tabs";
import { jsx as jsx100 } from "react/jsx-runtime";
var SidebarTabTriggers = ({
children,
...rest
}) => {
return /* @__PURE__ */ jsx100(RadixTabs.List, { className: "sidebar-triggers", ...rest, children });
};
SidebarTabTriggers.displayName = "SidebarTabTriggers";
// components/Sidebar/SidebarTabTrigger.tsx
import * as RadixTabs2 from "@radix-ui/react-tabs";
import { jsx as jsx101 } from "react/jsx-runtime";
var SidebarTabTrigger = ({
children,
tab,
onSelect,
...rest
}) => {
return /* @__PURE__ */ jsx101(RadixTabs2.Trigger, { value: tab, asChild: true, onSelect, children: /* @__PURE__ */ jsx101(
"button",
{
type: "button",
className: `excalidraw-button sidebar-tab-trigger`,
...rest,
children
}
) });
};
SidebarTabTrigger.displayName = "SidebarTabTrigger";
// components/Sidebar/SidebarTabs.tsx
import * as RadixTabs3 from "@radix-ui/react-tabs";
import { jsx as jsx102 } from "react/jsx-runtime";
var SidebarTabs = ({
children,
...rest
}) => {
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
if (!appState.openSidebar) {
return null;
}
const { name } = appState.openSidebar;
return /* @__PURE__ */ jsx102(
RadixTabs3.Root,
{
className: "sidebar-tabs-root",
value: appState.openSidebar.tab,
onValueChange: (tab) => setAppState((state) => ({
...state,
openSidebar: { ...state.openSidebar, name, tab }
})),
...rest,
children
}
);
};
SidebarTabs.displayName = "SidebarTabs";
// components/Sidebar/SidebarTab.tsx
import * as RadixTabs4 from "@radix-ui/react-tabs";
import { jsx as jsx103 } from "react/jsx-runtime";
var SidebarTab = ({
tab,
children,
...rest
}) => {
return /* @__PURE__ */ jsx103(RadixTabs4.Content, { ...rest, value: tab, "data-testid": tab, children });
};
SidebarTab.displayName = "SidebarTab";
// components/Sidebar/Sidebar.tsx
import { jsx as jsx104 } from "react/jsx-runtime";
import { createElement } from "react";
var isSidebarDockedAtom = atom(false);
var SidebarInner = forwardRef5(
({
name,
children,
onDock,
docked,
className,
...rest
}, ref) => {
if (define_import_meta_env_default.DEV && onDock && docked == null) {
console.warn(
"Sidebar: `docked` must be set when `onDock` is supplied for the sidebar to be user-dockable. To hide this message, either pass `docked` or remove `onDock`"
);
}
const setAppState = useExcalidrawSetAppState();
const setIsSidebarDockedAtom = useSetAtom(isSidebarDockedAtom);
useLayoutEffect7(() => {
setIsSidebarDockedAtom(!!docked);
return () => {
setIsSidebarDockedAtom(false);
};
}, [setIsSidebarDockedAtom, docked]);
const headerPropsRef = useRef27(
{}
);
headerPropsRef.current.onCloseRequest = () => {
setAppState({ openSidebar: null });
};
headerPropsRef.current.onDock = (isDocked) => onDock?.(isDocked);
headerPropsRef.current = updateObject(headerPropsRef.current, {
docked,
// explicit prop to rerender on update
shouldRenderDockButton: !!onDock && docked != null
});
const islandRef = useRef27(null);
useImperativeHandle2(ref, () => {
return islandRef.current;
});
const device = useDevice();
const closeLibrary = useCallback12(() => {
const isDialogOpen = !!document.querySelector(".Dialog");
if (isDialogOpen) {
return;
}
setAppState({ openSidebar: null });
}, [setAppState]);
useOutsideClick(
islandRef,
useCallback12(
(event) => {
if (event.target.closest(".sidebar-trigger")) {
return;
}
if (!docked || !device.editor.canFitSidebar) {
closeLibrary();
}
},
[closeLibrary, docked, device.editor.canFitSidebar]
)
);
useEffect32(() => {
const handleKeyDown = (event) => {
if (event.key === KEYS.ESCAPE && (!docked || !device.editor.canFitSidebar)) {
closeLibrary();
}
};
document.addEventListener("keydown" /* KEYDOWN */, handleKeyDown);
return () => {
document.removeEventListener("keydown" /* KEYDOWN */, handleKeyDown);
};
}, [closeLibrary, docked, device.editor.canFitSidebar]);
return /* @__PURE__ */ jsx104(
Island,
{
...rest,
className: clsx45("sidebar", { "sidebar--docked": docked }, className),
ref: islandRef,
children: /* @__PURE__ */ jsx104(SidebarPropsContext.Provider, { value: headerPropsRef.current, children })
}
);
}
);
SidebarInner.displayName = "SidebarInner";
var Sidebar = Object.assign(
forwardRef5((props, ref) => {
const appState = useUIAppState();
const { onStateChange } = props;
const refPrevOpenSidebar = useRef27(appState.openSidebar);
useEffect32(() => {
if (
// closing sidebar
(!appState.openSidebar && refPrevOpenSidebar?.current?.name === props.name || // opening current sidebar
appState.openSidebar?.name === props.name && refPrevOpenSidebar?.current?.name !== props.name || // switching tabs or switching to a different sidebar
refPrevOpenSidebar.current?.name === props.name) && appState.openSidebar !== refPrevOpenSidebar.current
) {
onStateChange?.(
appState.openSidebar?.name !== props.name ? null : appState.openSidebar
);
}
refPrevOpenSidebar.current = appState.openSidebar;
}, [appState.openSidebar, onStateChange, props.name]);
const [mounted, setMounted] = useState30(false);
useLayoutEffect7(() => {
setMounted(true);
return () => setMounted(false);
}, []);
const shouldRender = mounted && appState.openSidebar?.name === props.name;
if (!shouldRender) {
return null;
}
return /* @__PURE__ */ createElement(SidebarInner, { ...props, ref, key: props.name });
}),
{
Header: SidebarHeader,
TabTriggers: SidebarTabTriggers,
TabTrigger: SidebarTabTrigger,
Tabs: SidebarTabs,
Tab: SidebarTab,
Trigger: SidebarTrigger
}
);
Sidebar.displayName = "Sidebar";
// components/main-menu/DefaultItems.tsx
var DefaultItems_exports = {};
__export(DefaultItems_exports, {
ChangeCanvasBackground: () => ChangeCanvasBackground,
ClearCanvas: () => ClearCanvas,
CommandPalette: () => CommandPalette2,
Export: () => Export,
Help: () => Help,
LiveCollaborationTrigger: () => LiveCollaborationTrigger,
LoadScene: () => LoadScene,
SaveAsImage: () => SaveAsImage,
SaveToActiveFile: () => SaveToActiveFile,
SearchMenu: () => SearchMenu,
Socials: () => Socials,
ToggleTheme: () => ToggleTheme
});
import clsx46 from "clsx";
// components/OverwriteConfirm/OverwriteConfirmState.ts
var overwriteConfirmStateAtom = atom({
active: false
});
async function openConfirmModal({
title,
description,
actionLabel,
color
}) {
return new Promise((resolve) => {
editorJotaiStore.set(overwriteConfirmStateAtom, {
active: true,
onConfirm: () => resolve(true),
onClose: () => resolve(false),
onReject: () => resolve(false),
title,
description,
actionLabel,
color
});
});
}
// components/dropdownMenu/DropdownMenuItemContentRadio.tsx
import { Fragment as Fragment16, jsx as jsx105, jsxs as jsxs57 } from "react/jsx-runtime";
var DropdownMenuItemContentRadio = ({
value,
shortcut,
onChange,
choices,
children,
name
}) => {
const device = useDevice();
return /* @__PURE__ */ jsxs57(Fragment16, { children: [
/* @__PURE__ */ jsxs57("div", { className: "dropdown-menu-item-base dropdown-menu-item-bare", children: [
/* @__PURE__ */ jsx105("label", { className: "dropdown-menu-item__text", htmlFor: name, children }),
/* @__PURE__ */ jsx105(
RadioGroup,
{
name,
value,
onChange,
choices
}
)
] }),
shortcut && !device.editor.isMobile && /* @__PURE__ */ jsx105("div", { className: "dropdown-menu-item__shortcut dropdown-menu-item__shortcut--orphaned", children: shortcut })
] });
};
DropdownMenuItemContentRadio.displayName = "DropdownMenuItemContentRadio";
var DropdownMenuItemContentRadio_default = DropdownMenuItemContentRadio;
// components/main-menu/DefaultItems.tsx
import { Fragment as Fragment17, jsx as jsx106, jsxs as jsxs58 } from "react/jsx-runtime";
var LoadScene = () => {
const { t: t2 } = useI18n();
const actionManager = useExcalidrawActionManager();
const elements = useExcalidrawElements();
if (!actionManager.isActionEnabled(actionLoadScene)) {
return null;
}
const handleSelect = async () => {
if (!elements.length || await openConfirmModal({
title: t2("overwriteConfirm.modal.loadFromFile.title"),
actionLabel: t2("overwriteConfirm.modal.loadFromFile.button"),
color: "warning",
description: /* @__PURE__ */ jsx106(
Trans_default,
{
i18nKey: "overwriteConfirm.modal.loadFromFile.description",
bold: (text) => /* @__PURE__ */ jsx106("strong", { children: text }),
br: () => /* @__PURE__ */ jsx106("br", {})
}
)
})) {
actionManager.executeAction(actionLoadScene);
}
};
return /* @__PURE__ */ jsx106(
DropdownMenuItem_default,
{
icon: LoadIcon,
onSelect: handleSelect,
"data-testid": "load-button",
shortcut: getShortcutFromShortcutName("loadScene"),
"aria-label": t2("buttons.load"),
children: t2("buttons.load")
}
);
};
LoadScene.displayName = "LoadScene";
var SaveToActiveFile = () => {
const { t: t2 } = useI18n();
const actionManager = useExcalidrawActionManager();
if (!actionManager.isActionEnabled(actionSaveToActiveFile)) {
return null;
}
return /* @__PURE__ */ jsx106(
DropdownMenuItem_default,
{
shortcut: getShortcutFromShortcutName("saveScene"),
"data-testid": "save-button",
onSelect: () => actionManager.executeAction(actionSaveToActiveFile),
icon: save,
"aria-label": `${t2("buttons.save")}`,
children: `${t2("buttons.save")}`
}
);
};
SaveToActiveFile.displayName = "SaveToActiveFile";
var SaveAsImage = () => {
const setAppState = useExcalidrawSetAppState();
const { t: t2 } = useI18n();
return /* @__PURE__ */ jsx106(
DropdownMenuItem_default,
{
icon: ExportImageIcon,
"data-testid": "image-export-button",
onSelect: () => setAppState({ openDialog: { name: "imageExport" } }),
shortcut: getShortcutFromShortcutName("imageExport"),
"aria-label": t2("buttons.exportImage"),
children: t2("buttons.exportImage")
}
);
};
SaveAsImage.displayName = "SaveAsImage";
var CommandPalette2 = (opts) => {
const setAppState = useExcalidrawSetAppState();
const { t: t2 } = useI18n();
return /* @__PURE__ */ jsx106(
DropdownMenuItem_default,
{
icon: boltIcon,
"data-testid": "command-palette-button",
onSelect: () => {
trackEvent("command_palette", "open", "menu");
setAppState({ openDialog: { name: "commandPalette" } });
},
shortcut: getShortcutFromShortcutName("commandPalette"),
"aria-label": t2("commandPalette.title"),
className: opts?.className,
children: t2("commandPalette.title")
}
);
};
CommandPalette2.displayName = "CommandPalette";
var SearchMenu = (opts) => {
const { t: t2 } = useI18n();
const actionManager = useExcalidrawActionManager();
return /* @__PURE__ */ jsx106(
DropdownMenuItem_default,
{
icon: searchIcon,
"data-testid": "search-menu-button",
onSelect: () => {
actionManager.executeAction(actionToggleSearchMenu);
},
shortcut: getShortcutFromShortcutName("searchMenu"),
"aria-label": t2("search.title"),
className: opts?.className,
children: t2("search.title")
}
);
};
SearchMenu.displayName = "SearchMenu";
var Help = () => {
const { t: t2 } = useI18n();
const actionManager = useExcalidrawActionManager();
return /* @__PURE__ */ jsx106(
DropdownMenuItem_default,
{
"data-testid": "help-menu-item",
icon: HelpIcon,
onSelect: () => actionManager.executeAction(actionShortcuts),
shortcut: "?",
"aria-label": t2("helpDialog.title"),
children: t2("helpDialog.title")
}
);
};
Help.displayName = "Help";
var ClearCanvas = () => {
const { t: t2 } = useI18n();
const setActiveConfirmDialog = useSetAtom(activeConfirmDialogAtom);
const actionManager = useExcalidrawActionManager();
if (!actionManager.isActionEnabled(actionClearCanvas)) {
return null;
}
return /* @__PURE__ */ jsx106(
DropdownMenuItem_default,
{
icon: TrashIcon,
onSelect: () => setActiveConfirmDialog("clearCanvas"),
"data-testid": "clear-canvas-button",
"aria-label": t2("buttons.clearReset"),
children: t2("buttons.clearReset")
}
);
};
ClearCanvas.displayName = "ClearCanvas";
var ToggleTheme = (props) => {
const { t: t2 } = useI18n();
const appState = useUIAppState();
const actionManager = useExcalidrawActionManager();
const shortcut = getShortcutFromShortcutName("toggleTheme");
if (!actionManager.isActionEnabled(actionToggleTheme)) {
return null;
}
if (props?.allowSystemTheme) {
return /* @__PURE__ */ jsx106(
DropdownMenuItemContentRadio_default,
{
name: "theme",
value: props.theme,
onChange: (value) => props.onSelect(value),
choices: [
{
value: THEME.LIGHT,
label: SunIcon,
ariaLabel: `${t2("buttons.lightMode")} - ${shortcut}`
},
{
value: THEME.DARK,
label: MoonIcon,
ariaLabel: `${t2("buttons.darkMode")} - ${shortcut}`
},
{
value: "system",
label: DeviceDesktopIcon,
ariaLabel: t2("buttons.systemMode")
}
],
children: t2("labels.theme")
}
);
}
return /* @__PURE__ */ jsx106(
DropdownMenuItem_default,
{
onSelect: (event) => {
event.preventDefault();
if (props?.onSelect) {
props.onSelect(
appState.theme === THEME.DARK ? THEME.LIGHT : THEME.DARK
);
} else {
return actionManager.executeAction(actionToggleTheme);
}
},
icon: appState.theme === THEME.DARK ? SunIcon : MoonIcon,
"data-testid": "toggle-dark-mode",
shortcut,
"aria-label": appState.theme === THEME.DARK ? t2("buttons.lightMode") : t2("buttons.darkMode"),
children: appState.theme === THEME.DARK ? t2("buttons.lightMode") : t2("buttons.darkMode")
}
);
};
ToggleTheme.displayName = "ToggleTheme";
var ChangeCanvasBackground = () => {
const { t: t2 } = useI18n();
const appState = useUIAppState();
const actionManager = useExcalidrawActionManager();
const appProps = useAppProps();
if (appState.viewModeEnabled || !appProps.UIOptions.canvasActions.changeViewBackgroundColor) {
return null;
}
return /* @__PURE__ */ jsxs58("div", { style: { marginTop: "0.5rem" }, children: [
/* @__PURE__ */ jsx106(
"div",
{
"data-testid": "canvas-background-label",
style: { fontSize: ".75rem", marginBottom: ".5rem" },
children: t2("labels.canvasBackground")
}
),
/* @__PURE__ */ jsx106("div", { style: { padding: "0 0.625rem" }, children: actionManager.renderAction("changeViewBackgroundColor") })
] });
};
ChangeCanvasBackground.displayName = "ChangeCanvasBackground";
var Export = () => {
const { t: t2 } = useI18n();
const setAppState = useExcalidrawSetAppState();
return /* @__PURE__ */ jsx106(
DropdownMenuItem_default,
{
icon: ExportIcon,
onSelect: () => {
setAppState({ openDialog: { name: "jsonExport" } });
},
"data-testid": "json-export-button",
"aria-label": t2("buttons.export"),
children: t2("buttons.export")
}
);
};
Export.displayName = "Export";
var Socials = () => {
const { t: t2 } = useI18n();
return /* @__PURE__ */ jsxs58(Fragment17, { children: [
/* @__PURE__ */ jsx106(
DropdownMenuItemLink_default,
{
icon: GithubIcon,
href: "https://github.com/excalidraw/excalidraw",
"aria-label": "GitHub",
children: "GitHub"
}
),
/* @__PURE__ */ jsx106(
DropdownMenuItemLink_default,
{
icon: XBrandIcon,
href: "https://x.com/excalidraw",
"aria-label": "X",
children: t2("labels.followUs")
}
),
/* @__PURE__ */ jsx106(
DropdownMenuItemLink_default,
{
icon: DiscordIcon,
href: "https://discord.gg/UexuTaE",
"aria-label": "Discord",
children: t2("labels.discordChat")
}
)
] });
};
Socials.displayName = "Socials";
var LiveCollaborationTrigger = ({
onSelect,
isCollaborating
}) => {
const { t: t2 } = useI18n();
return /* @__PURE__ */ jsx106(
DropdownMenuItem_default,
{
"data-testid": "collab-button",
icon: usersIcon,
className: clsx46({
"active-collab": isCollaborating
}),
onSelect,
children: t2("labels.liveCollaboration")
}
);
};
LiveCollaborationTrigger.displayName = "LiveCollaborationTrigger";
// components/hoc/withInternalFallback.tsx
import { useLayoutEffect as useLayoutEffect8, useRef as useRef28 } from "react";
import { jsx as jsx107 } from "react/jsx-runtime";
var withInternalFallback = (componentName, Component) => {
const renderAtom = atom(0);
const WrapperComponent = (props) => {
const {
tunnelsJotai: { useAtom: useAtom2 }
} = useTunnels();
const [, setCounter] = useAtom2(renderAtom);
const metaRef = useRef28({
// flag set on initial render to tell the fallback component to skip the
// render until mount counter are initialized. This is because the counter
// is initialized in an effect, and thus we could end rendering both
// components at the same time until counter is initialized.
preferHost: false,
counter: 0
});
useLayoutEffect8(() => {
const meta = metaRef.current;
setCounter((c) => {
const next = c + 1;
meta.counter = next;
return next;
});
return () => {
setCounter((c) => {
const next = c - 1;
meta.counter = next;
if (!next) {
meta.preferHost = false;
}
return next;
});
};
}, [setCounter]);
if (!props.__fallback) {
metaRef.current.preferHost = true;
}
if (
// either before the counters are initialized
!metaRef.current.counter && props.__fallback && metaRef.current.preferHost || // or after the counters are initialized, and both are rendered
// (this is the default when host renders as well)
metaRef.current.counter > 1 && props.__fallback
) {
return null;
}
return /* @__PURE__ */ jsx107(Component, { ...props });
};
WrapperComponent.displayName = componentName;
return WrapperComponent;
};
// components/main-menu/MainMenu.tsx
import { jsx as jsx108, jsxs as jsxs59 } from "react/jsx-runtime";
var MainMenu = Object.assign(
withInternalFallback(
"MainMenu",
({
children,
onSelect
}) => {
const { MainMenuTunnel } = useTunnels();
const device = useDevice();
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const onClickOutside = device.editor.isMobile ? void 0 : () => setAppState({ openMenu: null });
return /* @__PURE__ */ jsx108(MainMenuTunnel.In, { children: /* @__PURE__ */ jsxs59(DropdownMenu_default, { open: appState.openMenu === "canvas", children: [
/* @__PURE__ */ jsx108(
DropdownMenu_default.Trigger,
{
onToggle: () => {
setAppState({
openMenu: appState.openMenu === "canvas" ? null : "canvas"
});
},
"data-testid": "main-menu-trigger",
className: "main-menu-trigger",
children: HamburgerMenuIcon
}
),
/* @__PURE__ */ jsxs59(
DropdownMenu_default.Content,
{
onClickOutside,
onSelect: composeEventHandlers(onSelect, () => {
setAppState({ openMenu: null });
}),
children: [
children,
device.editor.isMobile && appState.collaborators.size > 0 && /* @__PURE__ */ jsxs59("fieldset", { className: "UserList-Wrapper", children: [
/* @__PURE__ */ jsx108("legend", { children: t("labels.collaborators") }),
/* @__PURE__ */ jsx108(
UserList,
{
mobile: true,
collaborators: appState.collaborators,
userToFollow: appState.userToFollow?.socketId || null
}
)
] })
]
}
)
] }) });
}
),
{
Trigger: DropdownMenu_default.Trigger,
Item: DropdownMenu_default.Item,
ItemLink: DropdownMenu_default.ItemLink,
ItemCustom: DropdownMenu_default.ItemCustom,
Group: DropdownMenu_default.Group,
Separator: DropdownMenu_default.Separator,
DefaultItems: DefaultItems_exports
}
);
var MainMenu_default = MainMenu;
// components/OverwriteConfirm/OverwriteConfirmActions.tsx
import { jsx as jsx109, jsxs as jsxs60 } from "react/jsx-runtime";
var Action = ({
title,
children,
actionLabel,
onClick
}) => {
return /* @__PURE__ */ jsxs60("div", { className: "OverwriteConfirm__Actions__Action", children: [
/* @__PURE__ */ jsx109("h4", { children: title }),
/* @__PURE__ */ jsx109("div", { className: "OverwriteConfirm__Actions__Action__content", children }),
/* @__PURE__ */ jsx109(
FilledButton,
{
variant: "outlined",
color: "muted",
label: actionLabel,
size: "large",
fullWidth: true,
onClick
}
)
] });
};
var ExportToImage = () => {
const { t: t2 } = useI18n();
const actionManager = useExcalidrawActionManager();
const setAppState = useExcalidrawSetAppState();
return /* @__PURE__ */ jsx109(
Action,
{
title: t2("overwriteConfirm.action.exportToImage.title"),
actionLabel: t2("overwriteConfirm.action.exportToImage.button"),
onClick: () => {
actionManager.executeAction(actionChangeExportEmbedScene, "ui", true);
setAppState({ openDialog: { name: "imageExport" } });
},
children: t2("overwriteConfirm.action.exportToImage.description")
}
);
};
var SaveToDisk = () => {
const { t: t2 } = useI18n();
const actionManager = useExcalidrawActionManager();
return /* @__PURE__ */ jsx109(
Action,
{
title: t2("overwriteConfirm.action.saveToDisk.title"),
actionLabel: t2("overwriteConfirm.action.saveToDisk.button"),
onClick: () => {
actionManager.executeAction(actionSaveFileToDisk, "ui");
},
children: t2("overwriteConfirm.action.saveToDisk.description")
}
);
};
var Actions = Object.assign(
({ children }) => {
return /* @__PURE__ */ jsx109("div", { className: "OverwriteConfirm__Actions", children });
},
{
ExportToImage,
SaveToDisk
}
);
// components/OverwriteConfirm/OverwriteConfirm.tsx
import { jsx as jsx110, jsxs as jsxs61 } from "react/jsx-runtime";
var OverwriteConfirmDialog = Object.assign(
withInternalFallback(
"OverwriteConfirmDialog",
({ children }) => {
const { OverwriteConfirmDialogTunnel } = useTunnels();
const [overwriteConfirmState, setState] = useAtom(
overwriteConfirmStateAtom
);
if (!overwriteConfirmState.active) {
return null;
}
const handleClose = () => {
overwriteConfirmState.onClose();
setState((state) => ({ ...state, active: false }));
};
const handleConfirm = () => {
overwriteConfirmState.onConfirm();
setState((state) => ({ ...state, active: false }));
};
return /* @__PURE__ */ jsx110(OverwriteConfirmDialogTunnel.In, { children: /* @__PURE__ */ jsx110(Dialog, { onCloseRequest: handleClose, title: false, size: 916, children: /* @__PURE__ */ jsxs61("div", { className: "OverwriteConfirm", children: [
/* @__PURE__ */ jsx110("h3", { children: overwriteConfirmState.title }),
/* @__PURE__ */ jsxs61(
"div",
{
className: `OverwriteConfirm__Description OverwriteConfirm__Description--color-${overwriteConfirmState.color}`,
children: [
/* @__PURE__ */ jsx110("div", { className: "OverwriteConfirm__Description__icon", children: alertTriangleIcon }),
/* @__PURE__ */ jsx110("div", { children: overwriteConfirmState.description }),
/* @__PURE__ */ jsx110("div", { className: "OverwriteConfirm__Description__spacer" }),
/* @__PURE__ */ jsx110(
FilledButton,
{
color: overwriteConfirmState.color,
size: "large",
label: overwriteConfirmState.actionLabel,
onClick: handleConfirm
}
)
]
}
),
/* @__PURE__ */ jsx110(Actions, { children })
] }) }) });
}
),
{
Actions,
Action
}
);
// components/DefaultSidebar.tsx
import clsx48 from "clsx";
// components/SearchMenu.tsx
import { Fragment as Fragment18, memo as memo4, useEffect as useEffect33, useRef as useRef29, useState as useState31 } from "react";
import debounce2 from "lodash.debounce";
import clsx47 from "clsx";
import { Fragment as Fragment19, jsx as jsx111, jsxs as jsxs62 } from "react/jsx-runtime";
var searchQueryAtom = atom("");
var searchItemInFocusAtom = atom(null);
var SEARCH_DEBOUNCE = 350;
var SearchMenu2 = () => {
const app = useApp();
const setAppState = useExcalidrawSetAppState();
const searchInputRef = useRef29(null);
const [inputValue, setInputValue] = useAtom(searchQueryAtom);
const searchQuery = inputValue.trim();
const [isSearching, setIsSearching] = useState31(false);
const [searchMatches, setSearchMatches] = useState31({
nonce: null,
items: []
});
const searchedQueryRef = useRef29(null);
const lastSceneNonceRef = useRef29(void 0);
const [focusIndex, setFocusIndex] = useAtom(searchItemInFocusAtom);
const elementsMap = app.scene.getNonDeletedElementsMap();
useEffect33(() => {
if (isSearching) {
return;
}
if (searchQuery !== searchedQueryRef.current || app.scene.getSceneNonce() !== lastSceneNonceRef.current) {
searchedQueryRef.current = null;
handleSearch(searchQuery, app, (matchItems, index) => {
setSearchMatches({
nonce: randomInteger(),
items: matchItems
});
searchedQueryRef.current = searchQuery;
lastSceneNonceRef.current = app.scene.getSceneNonce();
setAppState({
searchMatches: matchItems.map((searchMatch) => ({
id: searchMatch.textElement.id,
focus: false,
matchedLines: searchMatch.matchedLines
}))
});
});
}
}, [
isSearching,
searchQuery,
elementsMap,
app,
setAppState,
setFocusIndex,
lastSceneNonceRef
]);
const goToNextItem = () => {
if (searchMatches.items.length > 0) {
setFocusIndex((focusIndex2) => {
if (focusIndex2 === null) {
return 0;
}
return (focusIndex2 + 1) % searchMatches.items.length;
});
}
};
const goToPreviousItem = () => {
if (searchMatches.items.length > 0) {
setFocusIndex((focusIndex2) => {
if (focusIndex2 === null) {
return 0;
}
return focusIndex2 - 1 < 0 ? searchMatches.items.length - 1 : focusIndex2 - 1;
});
}
};
useEffect33(() => {
setAppState((state) => {
return {
searchMatches: state.searchMatches.map((match, index) => {
if (index === focusIndex) {
return { ...match, focus: true };
}
return { ...match, focus: false };
})
};
});
}, [focusIndex, setAppState]);
useEffect33(() => {
if (searchMatches.items.length > 0 && focusIndex !== null) {
const match = searchMatches.items[focusIndex];
if (match) {
const zoomValue = app.state.zoom.value;
const matchAsElement = newTextElement({
text: match.searchQuery,
x: match.textElement.x + (match.matchedLines[0]?.offsetX ?? 0),
y: match.textElement.y + (match.matchedLines[0]?.offsetY ?? 0),
width: match.matchedLines[0]?.width,
height: match.matchedLines[0]?.height,
fontSize: match.textElement.fontSize,
fontFamily: match.textElement.fontFamily
});
const FONT_SIZE_LEGIBILITY_THRESHOLD = 14;
const fontSize = match.textElement.fontSize;
const isTextTiny = fontSize * zoomValue < FONT_SIZE_LEGIBILITY_THRESHOLD;
if (!isElementCompletelyInViewport(
[matchAsElement],
app.canvas.width / window.devicePixelRatio,
app.canvas.height / window.devicePixelRatio,
{
offsetLeft: app.state.offsetLeft,
offsetTop: app.state.offsetTop,
scrollX: app.state.scrollX,
scrollY: app.state.scrollY,
zoom: app.state.zoom
},
app.scene.getNonDeletedElementsMap(),
app.getEditorUIOffsets()
) || isTextTiny) {
let zoomOptions;
if (isTextTiny) {
if (fontSize >= FONT_SIZE_LEGIBILITY_THRESHOLD) {
zoomOptions = { fitToContent: true };
} else {
zoomOptions = {
fitToViewport: true,
// calculate zoom level to make the fontSize ~equal to FONT_SIZE_THRESHOLD, rounded to nearest 10%
maxZoom: round(FONT_SIZE_LEGIBILITY_THRESHOLD / fontSize, 1)
};
}
} else {
zoomOptions = { fitToContent: true };
}
app.scrollToContent(matchAsElement, {
animate: true,
duration: 300,
...zoomOptions,
canvasOffsets: app.getEditorUIOffsets()
});
}
}
}
}, [focusIndex, searchMatches, app]);
useEffect33(() => {
return () => {
setFocusIndex(null);
searchedQueryRef.current = null;
lastSceneNonceRef.current = void 0;
setAppState({
searchMatches: []
});
setIsSearching(false);
};
}, [setAppState, setFocusIndex]);
const stableState = useStable({
goToNextItem,
goToPreviousItem,
searchMatches
});
useEffect33(() => {
const eventHandler = (event) => {
if (event.key === KEYS.ESCAPE && !app.state.openDialog && !app.state.openPopup) {
event.preventDefault();
event.stopPropagation();
setAppState({
openSidebar: null
});
return;
}
if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.F) {
event.preventDefault();
event.stopPropagation();
if (!searchInputRef.current?.matches(":focus")) {
if (app.state.openDialog) {
setAppState({
openDialog: null
});
}
searchInputRef.current?.focus();
searchInputRef.current?.select();
} else {
setAppState({
openSidebar: null
});
}
}
if (event.target instanceof HTMLElement && event.target.closest(".layer-ui__search")) {
if (stableState.searchMatches.items.length) {
if (event.key === KEYS.ENTER) {
event.stopPropagation();
stableState.goToNextItem();
}
if (event.key === KEYS.ARROW_UP) {
event.stopPropagation();
stableState.goToPreviousItem();
} else if (event.key === KEYS.ARROW_DOWN) {
event.stopPropagation();
stableState.goToNextItem();
}
}
}
};
return addEventListener(window, "keydown" /* KEYDOWN */, eventHandler, {
capture: true,
passive: false
});
}, [setAppState, stableState, app]);
const matchCount = `${searchMatches.items.length} ${searchMatches.items.length === 1 ? t("search.singleResult") : t("search.multipleResults")}`;
return /* @__PURE__ */ jsxs62("div", { className: "layer-ui__search", children: [
/* @__PURE__ */ jsx111("div", { className: "layer-ui__search-header", children: /* @__PURE__ */ jsx111(
TextField,
{
className: CLASSES.SEARCH_MENU_INPUT_WRAPPER,
value: inputValue,
ref: searchInputRef,
placeholder: t("search.placeholder"),
icon: searchIcon,
onChange: (value) => {
setInputValue(value);
setIsSearching(true);
const searchQuery2 = value.trim();
handleSearch(searchQuery2, app, (matchItems, index) => {
setSearchMatches({
nonce: randomInteger(),
items: matchItems
});
setFocusIndex(index);
searchedQueryRef.current = searchQuery2;
lastSceneNonceRef.current = app.scene.getSceneNonce();
setAppState({
searchMatches: matchItems.map((searchMatch) => ({
id: searchMatch.textElement.id,
focus: false,
matchedLines: searchMatch.matchedLines
}))
});
setIsSearching(false);
});
},
selectOnRender: true
}
) }),
/* @__PURE__ */ jsxs62("div", { className: "layer-ui__search-count", children: [
searchMatches.items.length > 0 && /* @__PURE__ */ jsxs62(Fragment19, { children: [
focusIndex !== null && focusIndex > -1 ? /* @__PURE__ */ jsxs62("div", { children: [
focusIndex + 1,
" / ",
matchCount
] }) : /* @__PURE__ */ jsx111("div", { children: matchCount }),
/* @__PURE__ */ jsxs62("div", { className: "result-nav", children: [
/* @__PURE__ */ jsx111(
Button,
{
onSelect: () => {
goToNextItem();
},
className: "result-nav-btn",
children: collapseDownIcon
}
),
/* @__PURE__ */ jsx111(
Button,
{
onSelect: () => {
goToPreviousItem();
},
className: "result-nav-btn",
children: upIcon
}
)
] })
] }),
searchMatches.items.length === 0 && searchQuery && searchedQueryRef.current && /* @__PURE__ */ jsx111("div", { style: { margin: "1rem auto" }, children: t("search.noMatch") })
] }),
/* @__PURE__ */ jsx111(
MatchList,
{
matches: searchMatches,
onItemClick: setFocusIndex,
focusIndex,
searchQuery
}
)
] });
};
var ListItem = (props) => {
const preview = [
props.preview.moreBefore ? "..." : "",
props.preview.previewText.slice(0, props.preview.indexInSearchQuery),
props.preview.previewText.slice(
props.preview.indexInSearchQuery,
props.preview.indexInSearchQuery + props.searchQuery.length
),
props.preview.previewText.slice(
props.preview.indexInSearchQuery + props.searchQuery.length
),
props.preview.moreAfter ? "..." : ""
];
return /* @__PURE__ */ jsx111(
"div",
{
tabIndex: -1,
className: clsx47("layer-ui__result-item", {
active: props.highlighted
}),
onClick: props.onClick,
ref: (ref) => {
if (props.highlighted) {
ref?.scrollIntoView({ behavior: "auto", block: "nearest" });
}
},
children: /* @__PURE__ */ jsx111("div", { className: "preview-text", children: preview.flatMap((text, idx) => /* @__PURE__ */ jsx111(Fragment18, { children: idx === 2 ? /* @__PURE__ */ jsx111("b", { children: text }) : text }, idx)) })
}
);
};
var MatchListBase = (props) => {
return /* @__PURE__ */ jsx111("div", { className: "layer-ui__search-result-container", children: props.matches.items.map((searchMatch, index) => /* @__PURE__ */ jsx111(
ListItem,
{
searchQuery: props.searchQuery,
preview: searchMatch.preview,
highlighted: index === props.focusIndex,
onClick: () => props.onItemClick(index)
},
searchMatch.textElement.id + searchMatch.index
)) });
};
var areEqual = (prevProps, nextProps) => {
return prevProps.matches.nonce === nextProps.matches.nonce && prevProps.focusIndex === nextProps.focusIndex;
};
var MatchList = memo4(MatchListBase, areEqual);
var getMatchPreview = (text, index, searchQuery) => {
const WORDS_BEFORE = 2;
const WORDS_AFTER = 5;
const substrBeforeQuery = text.slice(0, index);
const wordsBeforeQuery = substrBeforeQuery.split(/\s+/);
const isQueryCompleteBefore = substrBeforeQuery.endsWith(" ");
const startWordIndex = wordsBeforeQuery.length - WORDS_BEFORE - 1 - (isQueryCompleteBefore ? 0 : 1);
let wordsBeforeAsString = wordsBeforeQuery.slice(startWordIndex <= 0 ? 0 : startWordIndex).join(" ") + (isQueryCompleteBefore ? " " : "");
const MAX_ALLOWED_CHARS = 20;
wordsBeforeAsString = wordsBeforeAsString.length > MAX_ALLOWED_CHARS ? wordsBeforeAsString.slice(-MAX_ALLOWED_CHARS) : wordsBeforeAsString;
const substrAfterQuery = text.slice(index + searchQuery.length);
const wordsAfter = substrAfterQuery.split(/\s+/);
const isQueryCompleteAfter = !substrAfterQuery.startsWith(" ");
const numberOfWordsToTake = isQueryCompleteAfter ? WORDS_AFTER + 1 : WORDS_AFTER;
const wordsAfterAsString = (isQueryCompleteAfter ? "" : " ") + wordsAfter.slice(0, numberOfWordsToTake).join(" ");
return {
indexInSearchQuery: wordsBeforeAsString.length,
previewText: wordsBeforeAsString + searchQuery + wordsAfterAsString,
moreBefore: startWordIndex > 0,
moreAfter: wordsAfter.length > numberOfWordsToTake
};
};
var normalizeWrappedText = (wrappedText, originalText) => {
const wrappedLines = wrappedText.split("\n");
const normalizedLines = [];
let originalIndex = 0;
for (let i = 0; i < wrappedLines.length; i++) {
let currentLine = wrappedLines[i];
const nextLine = wrappedLines[i + 1];
if (nextLine) {
const nextLineIndexInOriginal = originalText.indexOf(
nextLine,
originalIndex
);
if (nextLineIndexInOriginal > currentLine.length + originalIndex) {
let j = nextLineIndexInOriginal - (currentLine.length + originalIndex);
while (j > 0) {
currentLine += " ";
j--;
}
}
}
normalizedLines.push(currentLine);
originalIndex = originalIndex + currentLine.length;
}
return normalizedLines.join("\n");
};
var getMatchedLines = (textElement, searchQuery, index) => {
const normalizedText = normalizeWrappedText(
textElement.text,
textElement.originalText
);
const lines = normalizedText.split("\n");
const lineIndexRanges = [];
let currentIndex = 0;
let lineNumber = 0;
for (const line of lines) {
const startIndex2 = currentIndex;
const endIndex = startIndex2 + line.length - 1;
lineIndexRanges.push({
line,
startIndex: startIndex2,
endIndex,
lineNumber
});
currentIndex = endIndex + 1;
lineNumber++;
}
let startIndex = index;
let remainingQuery = textElement.originalText.slice(
index,
index + searchQuery.length
);
const matchedLines = [];
for (const lineIndexRange of lineIndexRanges) {
if (remainingQuery === "") {
break;
}
if (startIndex >= lineIndexRange.startIndex && startIndex <= lineIndexRange.endIndex) {
const matchCapacity = lineIndexRange.endIndex + 1 - startIndex;
const textToStart = lineIndexRange.line.slice(
0,
startIndex - lineIndexRange.startIndex
);
const matchedWord = remainingQuery.slice(0, matchCapacity);
remainingQuery = remainingQuery.slice(matchCapacity);
const offset = measureText(
textToStart,
getFontString(textElement),
textElement.lineHeight
);
if (textToStart === "") {
offset.width = 0;
}
if (textElement.textAlign !== "left" && lineIndexRange.line.length > 0) {
const lineLength = measureText(
lineIndexRange.line,
getFontString(textElement),
textElement.lineHeight
);
const spaceToStart = textElement.textAlign === "center" ? (textElement.width - lineLength.width) / 2 : textElement.width - lineLength.width;
offset.width += spaceToStart;
}
const { width, height } = measureText(
matchedWord,
getFontString(textElement),
textElement.lineHeight
);
const offsetX = offset.width;
const offsetY = lineIndexRange.lineNumber * offset.height;
matchedLines.push({
offsetX,
offsetY,
width,
height
});
startIndex += matchCapacity;
}
}
return matchedLines;
};
var escapeSpecialCharacters = (string) => {
return string.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
};
var handleSearch = debounce2(
(searchQuery, app, cb) => {
if (!searchQuery || searchQuery === "") {
cb([], null);
return;
}
const elements = app.scene.getNonDeletedElements();
const texts = elements.filter(
(el) => isTextElement(el)
);
texts.sort((a, b) => a.y - b.y);
const matchItems = [];
const regex = new RegExp(escapeSpecialCharacters(searchQuery), "gi");
for (const textEl of texts) {
let match = null;
const text = textEl.originalText;
while ((match = regex.exec(text)) !== null) {
const preview = getMatchPreview(text, match.index, searchQuery);
const matchedLines = getMatchedLines(textEl, searchQuery, match.index);
if (matchedLines.length > 0) {
matchItems.push({
textElement: textEl,
searchQuery,
preview,
index: match.index,
matchedLines
});
}
}
}
const visibleIds = new Set(
app.visibleElements.map((visibleElement) => visibleElement.id)
);
const focusIndex = matchItems.findIndex(
(matchItem) => visibleIds.has(matchItem.textElement.id)
) ?? null;
cb(matchItems, focusIndex);
},
SEARCH_DEBOUNCE
);
// components/DefaultSidebar.tsx
import { jsx as jsx112, jsxs as jsxs63 } from "react/jsx-runtime";
import { createElement as createElement2 } from "react";
var DefaultSidebarTrigger = withInternalFallback(
"DefaultSidebarTrigger",
(props) => {
const { DefaultSidebarTriggerTunnel } = useTunnels();
return /* @__PURE__ */ jsx112(DefaultSidebarTriggerTunnel.In, { children: /* @__PURE__ */ jsx112(
Sidebar.Trigger,
{
...props,
className: "default-sidebar-trigger",
name: DEFAULT_SIDEBAR.name
}
) });
}
);
DefaultSidebarTrigger.displayName = "DefaultSidebarTrigger";
var DefaultTabTriggers = ({ children }) => {
const { DefaultSidebarTabTriggersTunnel } = useTunnels();
return /* @__PURE__ */ jsx112(DefaultSidebarTabTriggersTunnel.In, { children });
};
DefaultTabTriggers.displayName = "DefaultTabTriggers";
var DefaultSidebar = Object.assign(
withInternalFallback(
"DefaultSidebar",
({
children,
className,
onDock,
docked,
...rest
}) => {
const appState = useUIAppState();
const setAppState = useExcalidrawSetAppState();
const { DefaultSidebarTabTriggersTunnel } = useTunnels();
const isForceDocked = appState.openSidebar?.tab === CANVAS_SEARCH_TAB;
return /* @__PURE__ */ createElement2(
Sidebar,
{
...rest,
name: "default",
key: "default",
className: clsx48("default-sidebar", className),
docked: isForceDocked || (docked ?? appState.defaultSidebarDockedPreference),
onDock: (
// `onDock=false` disables docking.
// if `docked` passed, but no onDock passed, disable manual docking.
isForceDocked || onDock === false || !onDock && docked != null ? void 0 : (
// compose to allow the host app to listen on default behavior
composeEventHandlers(onDock, (docked2) => {
setAppState({ defaultSidebarDockedPreference: docked2 });
})
)
)
},
/* @__PURE__ */ jsxs63(Sidebar.Tabs, { children: [
/* @__PURE__ */ jsx112(Sidebar.Header, { children: /* @__PURE__ */ jsxs63(Sidebar.TabTriggers, { children: [
/* @__PURE__ */ jsx112(Sidebar.TabTrigger, { tab: CANVAS_SEARCH_TAB, children: searchIcon }),
/* @__PURE__ */ jsx112(Sidebar.TabTrigger, { tab: LIBRARY_SIDEBAR_TAB, children: LibraryIcon }),
/* @__PURE__ */ jsx112(DefaultSidebarTabTriggersTunnel.Out, {})
] }) }),
/* @__PURE__ */ jsx112(Sidebar.Tab, { tab: LIBRARY_SIDEBAR_TAB, children: /* @__PURE__ */ jsx112(LibraryMenu, {}) }),
/* @__PURE__ */ jsx112(Sidebar.Tab, { tab: CANVAS_SEARCH_TAB, children: /* @__PURE__ */ jsx112(SearchMenu2, {}) }),
children
] })
);
}
),
{
Trigger: DefaultSidebarTrigger,
TabTriggers: DefaultTabTriggers
}
);
// components/LaserPointerButton.tsx
import clsx49 from "clsx";
import { jsx as jsx113, jsxs as jsxs64 } from "react/jsx-runtime";
var DEFAULT_SIZE3 = "small";
var LaserPointerButton = (props) => {
return /* @__PURE__ */ jsxs64(
"label",
{
className: clsx49(
"ToolIcon ToolIcon__LaserPointer",
`ToolIcon_size_${DEFAULT_SIZE3}`,
{
"is-mobile": props.isMobile
}
),
title: `${props.title}`,
children: [
/* @__PURE__ */ jsx113(
"input",
{
className: "ToolIcon_type_checkbox",
type: "checkbox",
name: props.name,
onChange: props.onChange,
checked: props.checked,
"aria-label": props.title,
"data-testid": "toolbar-LaserPointer"
}
),
/* @__PURE__ */ jsx113("div", { className: "ToolIcon__icon", children: laserPointerToolIcon })
]
}
);
};
// components/TTDDialog/MermaidToExcalidraw.tsx
import { useState as useState32, useRef as useRef31, useEffect as useEffect35, useDeferredValue } from "react";
// components/TTDDialog/common.ts
var resetPreview = ({
canvasRef,
setError
}) => {
const canvasNode = canvasRef.current;
if (!canvasNode) {
return;
}
const parent = canvasNode.parentElement;
if (!parent) {
return;
}
parent.style.background = "";
setError(null);
canvasNode.replaceChildren();
};
var convertMermaidToExcalidraw = async ({
canvasRef,
mermaidToExcalidrawLib,
mermaidDefinition,
setError,
data
}) => {
const canvasNode = canvasRef.current;
const parent = canvasNode?.parentElement;
if (!canvasNode || !parent) {
return;
}
if (!mermaidDefinition) {
resetPreview({ canvasRef, setError });
return;
}
try {
const api = await mermaidToExcalidrawLib.api;
let ret;
try {
ret = await api.parseMermaidToExcalidraw(mermaidDefinition);
} catch (err) {
ret = await api.parseMermaidToExcalidraw(
mermaidDefinition.replace(/"/g, "'")
);
}
const { elements, files } = ret;
setError(null);
data.current = {
elements: convertToExcalidrawElements(elements, {
regenerateIds: true
}),
files
};
const canvas = await exportToCanvas2({
elements: data.current.elements,
files: data.current.files,
exportPadding: DEFAULT_EXPORT_PADDING,
maxWidthOrHeight: Math.max(parent.offsetWidth, parent.offsetHeight) * window.devicePixelRatio
});
try {
await canvasToBlob(canvas);
} catch (e) {
if (e.name === "CANVAS_POSSIBLY_TOO_BIG") {
throw new Error(t("canvasError.canvasTooBig"));
}
throw e;
}
parent.style.background = "var(--default-bg-color)";
canvasNode.replaceChildren(canvas);
} catch (err) {
parent.style.background = "var(--default-bg-color)";
if (mermaidDefinition) {
setError(err);
}
throw err;
}
};
var saveMermaidDataToStorage = (mermaidDefinition) => {
EditorLocalStorage.set(
EDITOR_LS_KEYS.MERMAID_TO_EXCALIDRAW,
mermaidDefinition
);
};
var insertToEditor = ({
app,
data,
text,
shouldSaveMermaidDataToStorage
}) => {
const { elements: newElements, files } = data.current;
if (!newElements.length) {
return;
}
app.addElementsFromPasteOrLibrary({
elements: newElements,
files,
position: "center",
fitToContent: true
});
app.setOpenDialog(null);
if (shouldSaveMermaidDataToStorage && text) {
saveMermaidDataToStorage(text);
}
};
// components/TTDDialog/TTDDialogPanels.tsx
import { jsx as jsx114 } from "react/jsx-runtime";
var TTDDialogPanels = ({ children }) => {
return /* @__PURE__ */ jsx114("div", { className: "ttd-dialog-panels", children });
};
// components/TTDDialog/TTDDialogPanel.tsx
import clsx50 from "clsx";
import { jsx as jsx115, jsxs as jsxs65 } from "react/jsx-runtime";
var TTDDialogPanel = ({
label,
children,
panelAction,
panelActionDisabled = false,
onTextSubmitInProgess,
renderTopRight,
renderSubmitShortcut,
renderBottomRight
}) => {
return /* @__PURE__ */ jsxs65("div", { className: "ttd-dialog-panel", children: [
/* @__PURE__ */ jsxs65("div", { className: "ttd-dialog-panel__header", children: [
/* @__PURE__ */ jsx115("label", { children: label }),
renderTopRight?.()
] }),
children,
/* @__PURE__ */ jsxs65(
"div",
{
className: clsx50("ttd-dialog-panel-button-container", {
invisible: !panelAction
}),
style: { display: "flex", alignItems: "center" },
children: [
/* @__PURE__ */ jsxs65(
Button,
{
className: "ttd-dialog-panel-button",
onSelect: panelAction ? panelAction.action : () => {
},
disabled: panelActionDisabled || onTextSubmitInProgess,
children: [
/* @__PURE__ */ jsxs65("div", { className: clsx50({ invisible: onTextSubmitInProgess }), children: [
panelAction?.label,
panelAction?.icon && /* @__PURE__ */ jsx115("span", { children: panelAction.icon })
] }),
onTextSubmitInProgess && /* @__PURE__ */ jsx115(Spinner_default, {})
]
}
),
!panelActionDisabled && !onTextSubmitInProgess && renderSubmitShortcut?.(),
renderBottomRight?.()
]
}
)
] });
};
// components/TTDDialog/TTDDialogInput.tsx
import { useEffect as useEffect34, useRef as useRef30 } from "react";
import { jsx as jsx116 } from "react/jsx-runtime";
var TTDDialogInput = ({
input,
placeholder,
onChange,
onKeyboardSubmit
}) => {
const ref = useRef30(null);
const callbackRef = useRef30(onKeyboardSubmit);
callbackRef.current = onKeyboardSubmit;
useEffect34(() => {
if (!callbackRef.current) {
return;
}
const textarea = ref.current;
if (textarea) {
const handleKeyDown = (event) => {
if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.ENTER) {
event.preventDefault();
callbackRef.current?.();
}
};
textarea.addEventListener("keydown" /* KEYDOWN */, handleKeyDown);
return () => {
textarea.removeEventListener("keydown" /* KEYDOWN */, handleKeyDown);
};
}
}, []);
return /* @__PURE__ */ jsx116(
"textarea",
{
className: "ttd-dialog-input",
onChange,
value: input,
placeholder,
autoFocus: true,
ref
}
);
};
// components/TTDDialog/TTDDialogOutput.tsx
import { jsx as jsx117, jsxs as jsxs66 } from "react/jsx-runtime";
var ErrorComp = ({ error }) => {
return /* @__PURE__ */ jsxs66(
"div",
{
"data-testid": "ttd-dialog-output-error",
className: "ttd-dialog-output-error",
children: [
"Error! ",
/* @__PURE__ */ jsx117("p", { children: error })
]
}
);
};
var TTDDialogOutput = ({
error,
canvasRef,
loaded
}) => {
return /* @__PURE__ */ jsxs66("div", { className: "ttd-dialog-output-wrapper", children: [
error && /* @__PURE__ */ jsx117(ErrorComp, { error: error.message }),
loaded ? /* @__PURE__ */ jsx117(
"div",
{
ref: canvasRef,
style: { opacity: error ? "0.15" : 1 },
className: "ttd-dialog-output-canvas-container"
}
) : /* @__PURE__ */ jsx117(Spinner_default, { size: "2rem" })
] });
};
// components/TTDDialog/TTDDialogSubmitShortcut.tsx
import { jsx as jsx118, jsxs as jsxs67 } from "react/jsx-runtime";
var TTDDialogSubmitShortcut = () => {
return /* @__PURE__ */ jsxs67("div", { className: "ttd-dialog-submit-shortcut", children: [
/* @__PURE__ */ jsx118("div", { className: "ttd-dialog-submit-shortcut__key", children: getShortcutKey("CtrlOrCmd") }),
/* @__PURE__ */ jsx118("div", { className: "ttd-dialog-submit-shortcut__key", children: getShortcutKey("Enter") })
] });
};
// components/TTDDialog/MermaidToExcalidraw.tsx
import { Fragment as Fragment20, jsx as jsx119, jsxs as jsxs68 } from "react/jsx-runtime";
var MERMAID_EXAMPLE = "flowchart TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]";
var debouncedSaveMermaidDefinition = debounce(saveMermaidDataToStorage, 300);
var MermaidToExcalidraw = ({
mermaidToExcalidrawLib
}) => {
const [text, setText] = useState32(
() => EditorLocalStorage.get(EDITOR_LS_KEYS.MERMAID_TO_EXCALIDRAW) || MERMAID_EXAMPLE
);
const deferredText = useDeferredValue(text.trim());
const [error, setError] = useState32(null);
const canvasRef = useRef31(null);
const data = useRef31({ elements: [], files: null });
const app = useApp();
useEffect35(() => {
convertMermaidToExcalidraw({
canvasRef,
data,
mermaidToExcalidrawLib,
setError,
mermaidDefinition: deferredText
}).catch((err) => {
if (isDevEnv()) {
console.error("Failed to parse mermaid definition", err);
}
});
debouncedSaveMermaidDefinition(deferredText);
}, [deferredText, mermaidToExcalidrawLib]);
useEffect35(
() => () => {
debouncedSaveMermaidDefinition.flush();
},
[]
);
const onInsertToEditor = () => {
insertToEditor({
app,
data,
text,
shouldSaveMermaidDataToStorage: true
});
};
return /* @__PURE__ */ jsxs68(Fragment20, { children: [
/* @__PURE__ */ jsx119("div", { className: "ttd-dialog-desc", children: /* @__PURE__ */ jsx119(
Trans_default,
{
i18nKey: "mermaid.description",
flowchartLink: (el) => /* @__PURE__ */ jsx119("a", { href: "https://mermaid.js.org/syntax/flowchart.html", children: el }),
sequenceLink: (el) => /* @__PURE__ */ jsx119("a", { href: "https://mermaid.js.org/syntax/sequenceDiagram.html", children: el }),
classLink: (el) => /* @__PURE__ */ jsx119("a", { href: "https://mermaid.js.org/syntax/classDiagram.html", children: el })
}
) }),
/* @__PURE__ */ jsxs68(TTDDialogPanels, { children: [
/* @__PURE__ */ jsx119(TTDDialogPanel, { label: t("mermaid.syntax"), children: /* @__PURE__ */ jsx119(
TTDDialogInput,
{
input: text,
placeholder: "Write Mermaid diagram defintion here...",
onChange: (event) => setText(event.target.value),
onKeyboardSubmit: () => {
onInsertToEditor();
}
}
) }),
/* @__PURE__ */ jsx119(
TTDDialogPanel,
{
label: t("mermaid.preview"),
panelAction: {
action: () => {
onInsertToEditor();
},
label: t("mermaid.button"),
icon: ArrowRightIcon
},
renderSubmitShortcut: () => /* @__PURE__ */ jsx119(TTDDialogSubmitShortcut, {}),
children: /* @__PURE__ */ jsx119(
TTDDialogOutput,
{
canvasRef,
loaded: mermaidToExcalidrawLib.loaded,
error
}
)
}
)
] })
] });
};
var MermaidToExcalidraw_default = MermaidToExcalidraw;
// components/TTDDialog/TTDDialogTabs.tsx
import * as RadixTabs5 from "@radix-ui/react-tabs";
import { useRef as useRef32 } from "react";
import { jsx as jsx120 } from "react/jsx-runtime";
var TTDDialogTabs = (props) => {
const setAppState = useExcalidrawSetAppState();
const rootRef = useRef32(null);
const minHeightRef = useRef32(0);
return /* @__PURE__ */ jsx120(
RadixTabs5.Root,
{
ref: rootRef,
className: "ttd-dialog-tabs-root",
value: props.tab,
onValueChange: (tab) => {
if (!tab) {
return;
}
const modalContentNode = rootRef.current?.closest(".Modal__content");
if (modalContentNode) {
const currHeight = modalContentNode.offsetHeight || 0;
if (currHeight > minHeightRef.current) {
minHeightRef.current = currHeight;
modalContentNode.style.minHeight = `min(${minHeightRef.current}px, 100%)`;
}
}
if (props.dialog === "ttd" && isMemberOf(["text-to-diagram", "mermaid"], tab)) {
setAppState({
openDialog: { name: props.dialog, tab }
});
}
},
children: props.children
}
);
};
TTDDialogTabs.displayName = "TTDDialogTabs";
var TTDDialogTabs_default = TTDDialogTabs;
// components/TTDDialog/TTDDialog.tsx
import { useEffect as useEffect36, useRef as useRef33, useState as useState33 } from "react";
// components/TTDDialog/TTDDialogTabTriggers.tsx
import * as RadixTabs6 from "@radix-ui/react-tabs";
import { jsx as jsx121 } from "react/jsx-runtime";
var TTDDialogTabTriggers = ({
children,
...rest
}) => {
return /* @__PURE__ */ jsx121(RadixTabs6.List, { className: "ttd-dialog-triggers", ...rest, children });
};
TTDDialogTabTriggers.displayName = "TTDDialogTabTriggers";
// components/TTDDialog/TTDDialogTabTrigger.tsx
import * as RadixTabs7 from "@radix-ui/react-tabs";
import { jsx as jsx122 } from "react/jsx-runtime";
var TTDDialogTabTrigger = ({
children,
tab,
onSelect,
...rest
}) => {
return /* @__PURE__ */ jsx122(RadixTabs7.Trigger, { value: tab, asChild: true, onSelect, children: /* @__PURE__ */ jsx122("button", { type: "button", className: "ttd-dialog-tab-trigger", ...rest, children }) });
};
TTDDialogTabTrigger.displayName = "TTDDialogTabTrigger";
// components/TTDDialog/TTDDialogTab.tsx
import * as RadixTabs8 from "@radix-ui/react-tabs";
import { jsx as jsx123 } from "react/jsx-runtime";
var TTDDialogTab = ({
tab,
children,
...rest
}) => {
return /* @__PURE__ */ jsx123(RadixTabs8.Content, { ...rest, value: tab, children });
};
TTDDialogTab.displayName = "TTDDialogTab";
// components/TTDDialog/TTDDialog.tsx
import { jsx as jsx124, jsxs as jsxs69 } from "react/jsx-runtime";
var MIN_PROMPT_LENGTH = 3;
var MAX_PROMPT_LENGTH = 1e3;
var rateLimitsAtom = atom(null);
var ttdGenerationAtom = atom(null);
var TTDDialog = (props) => {
const appState = useUIAppState();
if (appState.openDialog?.name !== "ttd") {
return null;
}
return /* @__PURE__ */ jsx124(TTDDialogBase, { ...props, tab: appState.openDialog.tab });
};
var TTDDialogBase = withInternalFallback(
"TTDDialogBase",
({
tab,
...rest
}) => {
const app = useApp();
const setAppState = useExcalidrawSetAppState();
const someRandomDivRef = useRef33(null);
const [ttdGeneration, setTtdGeneration] = useAtom(ttdGenerationAtom);
const [text, setText] = useState33(ttdGeneration?.prompt ?? "");
const prompt = text.trim();
const handleTextChange = (event) => {
setText(event.target.value);
setTtdGeneration((s) => ({
generatedResponse: s?.generatedResponse ?? null,
prompt: event.target.value
}));
};
const [onTextSubmitInProgess, setOnTextSubmitInProgess] = useState33(false);
const [rateLimits, setRateLimits] = useAtom(rateLimitsAtom);
const onGenerate = async () => {
if (prompt.length > MAX_PROMPT_LENGTH || prompt.length < MIN_PROMPT_LENGTH || onTextSubmitInProgess || rateLimits?.rateLimitRemaining === 0 || // means this is not a text-to-diagram dialog (needed for TS only)
"__fallback" in rest) {
if (prompt.length < MIN_PROMPT_LENGTH) {
setError(
new Error(
`Prompt is too short (min ${MIN_PROMPT_LENGTH} characters)`
)
);
}
if (prompt.length > MAX_PROMPT_LENGTH) {
setError(
new Error(
`Prompt is too long (max ${MAX_PROMPT_LENGTH} characters)`
)
);
}
return;
}
try {
setOnTextSubmitInProgess(true);
trackEvent("ai", "generate", "ttd");
const { generatedResponse, error: error2, rateLimit, rateLimitRemaining } = await rest.onTextSubmit(prompt);
if (typeof generatedResponse === "string") {
setTtdGeneration((s) => ({
generatedResponse,
prompt: s?.prompt ?? null
}));
}
if (isFiniteNumber(rateLimit) && isFiniteNumber(rateLimitRemaining)) {
setRateLimits({ rateLimit, rateLimitRemaining });
}
if (error2) {
setError(error2);
return;
}
if (!generatedResponse) {
setError(new Error("Generation failed"));
return;
}
try {
await convertMermaidToExcalidraw({
canvasRef: someRandomDivRef,
data,
mermaidToExcalidrawLib,
setError,
mermaidDefinition: generatedResponse
});
trackEvent("ai", "mermaid parse success", "ttd");
} catch (error3) {
console.info(
`%cTTD mermaid render errror: ${error3.message}`,
"color: red"
);
console.info(
`>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
TTD mermaid definition render errror: ${error3.message}`,
"color: yellow"
);
trackEvent("ai", "mermaid parse failed", "ttd");
setError(
new Error(
"Generated an invalid diagram :(. You may also try a different prompt."
)
);
}
} catch (error2) {
let message = error2.message;
if (!message || message === "Failed to fetch") {
message = "Request failed";
}
setError(new Error(message));
} finally {
setOnTextSubmitInProgess(false);
}
};
const refOnGenerate = useRef33(onGenerate);
refOnGenerate.current = onGenerate;
const [mermaidToExcalidrawLib, setMermaidToExcalidrawLib] = useState33({
loaded: false,
api: import("@excalidraw/mermaid-to-excalidraw")
});
useEffect36(() => {
const fn = async () => {
await mermaidToExcalidrawLib.api;
setMermaidToExcalidrawLib((prev) => ({ ...prev, loaded: true }));
};
fn();
}, [mermaidToExcalidrawLib.api]);
const data = useRef33({ elements: [], files: null });
const [error, setError] = useState33(null);
return /* @__PURE__ */ jsx124(
Dialog,
{
className: "ttd-dialog",
onCloseRequest: () => {
app.setOpenDialog(null);
},
size: 1200,
title: false,
...rest,
autofocus: false,
children: /* @__PURE__ */ jsxs69(TTDDialogTabs_default, { dialog: "ttd", tab, children: [
"__fallback" in rest && rest.__fallback ? /* @__PURE__ */ jsx124("p", { className: "dialog-mermaid-title", children: t("mermaid.title") }) : /* @__PURE__ */ jsxs69(TTDDialogTabTriggers, { children: [
/* @__PURE__ */ jsx124(TTDDialogTabTrigger, { tab: "text-to-diagram", children: /* @__PURE__ */ jsxs69("div", { style: { display: "flex", alignItems: "center" }, children: [
t("labels.textToDiagram"),
/* @__PURE__ */ jsx124(
"div",
{
style: {
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: "1px 6px",
marginLeft: "10px",
fontSize: 10,
borderRadius: "12px",
background: "var(--color-promo)",
color: "var(--color-surface-lowest)"
},
children: "AI Beta"
}
)
] }) }),
/* @__PURE__ */ jsx124(TTDDialogTabTrigger, { tab: "mermaid", children: "Mermaid" })
] }),
/* @__PURE__ */ jsx124(TTDDialogTab, { className: "ttd-dialog-content", tab: "mermaid", children: /* @__PURE__ */ jsx124(
MermaidToExcalidraw_default,
{
mermaidToExcalidrawLib
}
) }),
!("__fallback" in rest) && /* @__PURE__ */ jsxs69(TTDDialogTab, { className: "ttd-dialog-content", tab: "text-to-diagram", children: [
/* @__PURE__ */ jsx124("div", { className: "ttd-dialog-desc", children: "Currently we use Mermaid as a middle step, so you'll get best results if you describe a diagram, workflow, flow chart, and similar." }),
/* @__PURE__ */ jsxs69(TTDDialogPanels, { children: [
/* @__PURE__ */ jsx124(
TTDDialogPanel,
{
label: t("labels.prompt"),
panelAction: {
action: onGenerate,
label: "Generate",
icon: ArrowRightIcon
},
onTextSubmitInProgess,
panelActionDisabled: prompt.length > MAX_PROMPT_LENGTH || rateLimits?.rateLimitRemaining === 0,
renderTopRight: () => {
if (!rateLimits) {
return null;
}
return /* @__PURE__ */ jsxs69(
"div",
{
className: "ttd-dialog-rate-limit",
style: {
fontSize: 12,
marginLeft: "auto",
color: rateLimits.rateLimitRemaining === 0 ? "var(--color-danger)" : void 0
},
children: [
rateLimits.rateLimitRemaining,
" requests left today"
]
}
);
},
renderSubmitShortcut: () => /* @__PURE__ */ jsx124(TTDDialogSubmitShortcut, {}),
renderBottomRight: () => {
if (typeof ttdGeneration?.generatedResponse === "string") {
return /* @__PURE__ */ jsxs69(
"div",
{
className: "excalidraw-link",
style: { marginLeft: "auto", fontSize: 14 },
onClick: () => {
if (typeof ttdGeneration?.generatedResponse === "string") {
saveMermaidDataToStorage(
ttdGeneration.generatedResponse
);
setAppState({
openDialog: { name: "ttd", tab: "mermaid" }
});
}
},
children: [
"View as Mermaid",
/* @__PURE__ */ jsx124(InlineIcon, { icon: ArrowRightIcon })
]
}
);
}
const ratio = prompt.length / MAX_PROMPT_LENGTH;
if (ratio > 0.8) {
return /* @__PURE__ */ jsxs69(
"div",
{
style: {
marginLeft: "auto",
fontSize: 12,
fontFamily: "monospace",
color: ratio > 1 ? "var(--color-danger)" : void 0
},
children: [
"Length: ",
prompt.length,
"/",
MAX_PROMPT_LENGTH
]
}
);
}
return null;
},
children: /* @__PURE__ */ jsx124(
TTDDialogInput,
{
onChange: handleTextChange,
input: text,
placeholder: "Describe what you want to see...",
onKeyboardSubmit: () => {
refOnGenerate.current();
}
}
)
}
),
/* @__PURE__ */ jsx124(
TTDDialogPanel,
{
label: "Preview",
panelAction: {
action: () => {
console.info("Panel action clicked");
insertToEditor({ app, data });
},
label: "Insert",
icon: ArrowRightIcon
},
children: /* @__PURE__ */ jsx124(
TTDDialogOutput,
{
canvasRef: someRandomDivRef,
error,
loaded: mermaidToExcalidrawLib.loaded
}
)
}
)
] })
] })
] })
}
);
}
);
// components/Stats/index.tsx
import { useEffect as useEffect38, useMemo as useMemo9, useState as useState35, memo as memo5 } from "react";
import throttle2 from "lodash.throttle";
// components/Stats/DragInput.tsx
import { useEffect as useEffect37, useRef as useRef34, useState as useState34 } from "react";
import clsx51 from "clsx";
// components/Stats/utils.ts
var SMALLEST_DELTA = 0.01;
var isPropertyEditable = (element, property) => {
if (property === "height" && isTextElement(element)) {
return false;
}
if (property === "width" && isTextElement(element)) {
return false;
}
if (property === "angle" && isFrameLikeElement(element)) {
return false;
}
return true;
};
var getStepSizedValue = (value, stepSize) => {
const v = value + stepSize / 2;
return v - v % stepSize;
};
var getElementsInAtomicUnit = (atomicUnit, elementsMap, originalElementsMap) => {
return Object.keys(atomicUnit).map((id) => ({
original: (originalElementsMap ?? elementsMap).get(id),
latest: elementsMap.get(id)
})).filter((el) => el.original !== void 0 && el.latest !== void 0);
};
var moveElement = (newTopLeftX, newTopLeftY, originalElement, elementsMap, elements, scene, originalElementsMap, shouldInformMutation = true) => {
const latestElement = elementsMap.get(originalElement.id);
if (!latestElement) {
return;
}
const [cx, cy] = [
originalElement.x + originalElement.width / 2,
originalElement.y + originalElement.height / 2
];
const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(originalElement.x, originalElement.y),
pointFrom(cx, cy),
originalElement.angle
);
const changeInX = newTopLeftX - topLeftX;
const changeInY = newTopLeftY - topLeftY;
const [x, y] = pointRotateRads(
pointFrom(newTopLeftX, newTopLeftY),
pointFrom(cx + changeInX, cy + changeInY),
-originalElement.angle
);
mutateElement(
latestElement,
{
x,
y
},
shouldInformMutation
);
updateBindings(latestElement, elementsMap, elements, scene);
const boundTextElement = getBoundTextElement(
originalElement,
originalElementsMap
);
if (boundTextElement) {
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
latestBoundTextElement && mutateElement(
latestBoundTextElement,
{
x: boundTextElement.x + changeInX,
y: boundTextElement.y + changeInY
},
shouldInformMutation
);
}
};
var getAtomicUnits = (targetElements, appState) => {
const selectedGroupIds = getSelectedGroupIds(appState);
const _atomicUnits = selectedGroupIds.map((gid) => {
return getElementsInGroup(targetElements, gid).reduce((acc, el) => {
acc[el.id] = true;
return acc;
}, {});
});
targetElements.filter((el) => !isInGroup(el)).forEach((el) => {
_atomicUnits.push({
[el.id]: true
});
});
return _atomicUnits;
};
var updateBindings = (latestElement, elementsMap, elements, scene, options) => {
if (isLinearElement(latestElement)) {
bindOrUnbindLinearElements(
[latestElement],
elementsMap,
elements,
scene,
true,
[],
options?.zoom
);
} else {
updateBoundElements(latestElement, elementsMap, options);
}
};
// components/Stats/DragInput.tsx
import { jsx as jsx125, jsxs as jsxs70 } from "react/jsx-runtime";
var StatsDragInput = ({
label,
icon,
dragInputCallback,
value,
elements,
editable = true,
shouldKeepAspectRatio,
property,
scene,
appState,
sensitivity = 1
}) => {
const app = useApp();
const inputRef = useRef34(null);
const labelRef = useRef34(null);
const [inputValue, setInputValue] = useState34(value.toString());
const stateRef = useRef34(null);
if (!stateRef.current) {
stateRef.current = {
originalAppState: cloneJSON(appState),
originalElements: elements,
lastUpdatedValue: inputValue,
updatePending: false
};
}
useEffect37(() => {
const inputValue2 = value.toString();
setInputValue(inputValue2);
stateRef.current.lastUpdatedValue = inputValue2;
}, [value]);
const handleInputValue = (updatedValue, elements2, appState2) => {
if (!stateRef.current.updatePending) {
return false;
}
stateRef.current.updatePending = false;
const parsed = Number(updatedValue);
if (isNaN(parsed)) {
setInputValue(value.toString());
return;
}
const rounded = Number(parsed.toFixed(2));
const original = Number(value);
if (isNaN(original) || Math.abs(rounded - original) >= SMALLEST_DELTA) {
stateRef.current.lastUpdatedValue = updatedValue;
dragInputCallback({
accumulatedChange: 0,
instantChange: 0,
originalElements: elements2,
originalElementsMap: app.scene.getNonDeletedElementsMap(),
shouldKeepAspectRatio,
shouldChangeByStepSize: false,
scene,
nextValue: rounded,
property,
originalAppState: appState2,
setInputValue: (value2) => setInputValue(String(value2))
});
app.syncActionResult({
captureUpdate: CaptureUpdateAction.IMMEDIATELY
});
}
};
const callbacksRef = useRef34({});
callbacksRef.current.handleInputValue = handleInputValue;
useEffect37(() => {
const input = inputRef.current;
const callbacks = callbacksRef.current;
return () => {
const nextValue = input?.value;
if (nextValue) {
callbacks.handleInputValue?.(
nextValue,
stateRef.current.originalElements,
stateRef.current.originalAppState
);
}
window.removeEventListener(
"pointermove" /* POINTER_MOVE */,
callbacks.onPointerMove,
false
);
window.removeEventListener(
"pointerup" /* POINTER_UP */,
callbacks.onPointerUp,
false
);
};
}, [
// we need to track change of `editable` state as mount/unmount
// because react doesn't trigger `blur` when a an input is blurred due
// to being disabled (https://github.com/facebook/react/issues/9142).
// As such, if we keep rendering disabled inputs, then change in selection
// to an element that has a given property as non-editable would not trigger
// blur/unmount and wouldn't update the value.
editable
]);
if (!editable) {
return null;
}
return /* @__PURE__ */ jsxs70(
"div",
{
className: clsx51("drag-input-container", !editable && "disabled"),
"data-testid": label,
children: [
/* @__PURE__ */ jsx125(
"div",
{
className: "drag-input-label",
ref: labelRef,
onPointerDown: (event) => {
if (inputRef.current && editable) {
document.body.classList.add("excalidraw-cursor-resize");
let startValue = Number(inputRef.current.value);
if (isNaN(startValue)) {
startValue = 0;
}
let lastPointer = null;
let originalElementsMap = app.scene.getNonDeletedElements().reduce((acc, element) => {
acc.set(element.id, deepCopyElement(element));
return acc;
}, /* @__PURE__ */ new Map());
let originalElements = elements.map(
(element) => originalElementsMap.get(element.id)
);
const originalAppState = cloneJSON(appState);
let accumulatedChange = 0;
let stepChange = 0;
const onPointerMove = (event2) => {
if (lastPointer && originalElementsMap !== null && originalElements !== null) {
const instantChange = event2.clientX - lastPointer.x;
if (instantChange !== 0) {
stepChange += instantChange;
if (Math.abs(stepChange) >= sensitivity) {
stepChange = Math.sign(stepChange) * Math.floor(Math.abs(stepChange) / sensitivity);
accumulatedChange += stepChange;
dragInputCallback({
accumulatedChange,
instantChange: stepChange,
originalElements,
originalElementsMap,
shouldKeepAspectRatio,
shouldChangeByStepSize: event2.shiftKey,
property,
scene,
originalAppState,
setInputValue: (value2) => setInputValue(String(value2))
});
stepChange = 0;
}
}
}
lastPointer = {
x: event2.clientX,
y: event2.clientY
};
};
const onPointerUp = () => {
window.removeEventListener(
"pointermove" /* POINTER_MOVE */,
onPointerMove,
false
);
app.syncActionResult({
captureUpdate: CaptureUpdateAction.IMMEDIATELY
});
lastPointer = null;
accumulatedChange = 0;
stepChange = 0;
originalElements = null;
originalElementsMap = null;
document.body.classList.remove("excalidraw-cursor-resize");
window.removeEventListener("pointerup" /* POINTER_UP */, onPointerUp, false);
};
callbacksRef.current.onPointerMove = onPointerMove;
callbacksRef.current.onPointerUp = onPointerUp;
window.addEventListener("pointermove" /* POINTER_MOVE */, onPointerMove, false);
window.addEventListener("pointerup" /* POINTER_UP */, onPointerUp, false);
}
},
onPointerEnter: () => {
if (labelRef.current) {
labelRef.current.style.cursor = "ew-resize";
}
},
children: icon ? /* @__PURE__ */ jsx125(InlineIcon, { icon }) : label
}
),
/* @__PURE__ */ jsx125(
"input",
{
className: "drag-input",
autoComplete: "off",
spellCheck: "false",
onKeyDown: (event) => {
if (editable) {
const eventTarget = event.target;
if (eventTarget instanceof HTMLInputElement && event.key === KEYS.ENTER) {
handleInputValue(eventTarget.value, elements, appState);
app.focusContainer();
}
}
},
ref: inputRef,
value: inputValue,
onChange: (event) => {
stateRef.current.updatePending = true;
setInputValue(event.target.value);
},
onFocus: (event) => {
event.target.select();
stateRef.current.originalElements = elements;
stateRef.current.originalAppState = cloneJSON(appState);
},
onBlur: (event) => {
if (!inputValue) {
setInputValue(value.toString());
} else if (editable) {
handleInputValue(
event.target.value,
stateRef.current.originalElements,
stateRef.current.originalAppState
);
}
},
disabled: !editable
}
)
]
}
);
};
var DragInput_default = StatsDragInput;
// components/Stats/Dimension.tsx
import { jsx as jsx126 } from "react/jsx-runtime";
var STEP_SIZE = 10;
var _shouldKeepAspectRatio = (element) => {
return element.type === "image";
};
var handleDimensionChange = ({
accumulatedChange,
originalElements,
originalElementsMap,
shouldKeepAspectRatio,
shouldChangeByStepSize,
nextValue,
property,
originalAppState,
instantChange,
scene
}) => {
const elementsMap = scene.getNonDeletedElementsMap();
const origElement = originalElements[0];
const latestElement = elementsMap.get(origElement.id);
if (origElement && latestElement) {
const keepAspectRatio = shouldKeepAspectRatio || _shouldKeepAspectRatio(origElement);
const aspectRatio = origElement.width / origElement.height;
if (originalAppState.croppingElementId === origElement.id) {
const element = elementsMap.get(origElement.id);
if (!element || !isImageElement(element) || !element.crop) {
return;
}
const crop = element.crop;
let nextCrop = { ...crop };
const isFlippedByX = element.scale[0] === -1;
const isFlippedByY = element.scale[1] === -1;
const { width: uncroppedWidth, height: uncroppedHeight } = getUncroppedWidthAndHeight(element);
const naturalToUncroppedWidthRatio = crop.naturalWidth / uncroppedWidth;
const naturalToUncroppedHeightRatio = crop.naturalHeight / uncroppedHeight;
const MAX_POSSIBLE_WIDTH = isFlippedByX ? crop.width + crop.x : crop.naturalWidth - crop.x;
const MAX_POSSIBLE_HEIGHT = isFlippedByY ? crop.height + crop.y : crop.naturalHeight - crop.y;
const MIN_WIDTH = MINIMAL_CROP_SIZE * naturalToUncroppedWidthRatio;
const MIN_HEIGHT = MINIMAL_CROP_SIZE * naturalToUncroppedHeightRatio;
if (nextValue !== void 0) {
if (property === "width") {
const nextValueInNatural = nextValue * naturalToUncroppedWidthRatio;
const nextCropWidth2 = clamp(
nextValueInNatural,
MIN_WIDTH,
MAX_POSSIBLE_WIDTH
);
nextCrop = {
...nextCrop,
width: nextCropWidth2,
x: isFlippedByX ? crop.x + crop.width - nextCropWidth2 : crop.x
};
} else if (property === "height") {
const nextValueInNatural = nextValue * naturalToUncroppedHeightRatio;
const nextCropHeight2 = clamp(
nextValueInNatural,
MIN_HEIGHT,
MAX_POSSIBLE_HEIGHT
);
nextCrop = {
...nextCrop,
height: nextCropHeight2,
y: isFlippedByY ? crop.y + crop.height - nextCropHeight2 : crop.y
};
}
mutateElement(element, {
crop: nextCrop,
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight)
});
return;
}
const changeInWidth2 = property === "width" ? instantChange : 0;
const changeInHeight2 = property === "height" ? instantChange : 0;
const nextCropWidth = clamp(
crop.width + changeInWidth2,
MIN_WIDTH,
MAX_POSSIBLE_WIDTH
);
const nextCropHeight = clamp(
crop.height + changeInHeight2,
MIN_WIDTH,
MAX_POSSIBLE_HEIGHT
);
nextCrop = {
...crop,
x: isFlippedByX ? crop.x + crop.width - nextCropWidth : crop.x,
y: isFlippedByY ? crop.y + crop.height - nextCropHeight : crop.y,
width: nextCropWidth,
height: nextCropHeight
};
mutateElement(element, {
crop: nextCrop,
width: nextCrop.width / (crop.naturalWidth / uncroppedWidth),
height: nextCrop.height / (crop.naturalHeight / uncroppedHeight)
});
return;
}
if (nextValue !== void 0) {
const nextWidth2 = Math.max(
property === "width" ? nextValue : keepAspectRatio ? nextValue * aspectRatio : origElement.width,
MIN_WIDTH_OR_HEIGHT
);
const nextHeight2 = Math.max(
property === "height" ? nextValue : keepAspectRatio ? nextValue / aspectRatio : origElement.height,
MIN_WIDTH_OR_HEIGHT
);
resizeSingleElement(
nextWidth2,
nextHeight2,
latestElement,
origElement,
elementsMap,
originalElementsMap,
property === "width" ? "e" : "s",
{
shouldMaintainAspectRatio: keepAspectRatio
}
);
return;
}
const changeInWidth = property === "width" ? accumulatedChange : 0;
const changeInHeight = property === "height" ? accumulatedChange : 0;
let nextWidth = Math.max(0, origElement.width + changeInWidth);
if (property === "width") {
if (shouldChangeByStepSize) {
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE);
} else {
nextWidth = Math.round(nextWidth);
}
}
let nextHeight = Math.max(0, origElement.height + changeInHeight);
if (property === "height") {
if (shouldChangeByStepSize) {
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE);
} else {
nextHeight = Math.round(nextHeight);
}
}
if (keepAspectRatio) {
if (property === "width") {
nextHeight = Math.round(nextWidth / aspectRatio * 100) / 100;
} else {
nextWidth = Math.round(nextHeight * aspectRatio * 100) / 100;
}
}
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
resizeSingleElement(
nextWidth,
nextHeight,
latestElement,
origElement,
elementsMap,
originalElementsMap,
property === "width" ? "e" : "s",
{
shouldMaintainAspectRatio: keepAspectRatio
}
);
}
};
var DimensionDragInput = ({
property,
element,
scene,
appState
}) => {
let value = round(property === "width" ? element.width : element.height, 2);
if (appState.croppingElementId && appState.croppingElementId === element.id && isImageElement(element) && element.crop) {
const { width: uncroppedWidth, height: uncroppedHeight } = getUncroppedWidthAndHeight(element);
if (property === "width") {
const ratio = uncroppedWidth / element.crop.naturalWidth;
value = round(element.crop.width * ratio, 2);
}
if (property === "height") {
const ratio = uncroppedHeight / element.crop.naturalHeight;
value = round(element.crop.height * ratio, 2);
}
}
return /* @__PURE__ */ jsx126(
DragInput_default,
{
label: property === "width" ? "W" : "H",
elements: [element],
dragInputCallback: handleDimensionChange,
value,
editable: isPropertyEditable(element, property),
scene,
appState,
property
}
);
};
var Dimension_default = DimensionDragInput;
// components/Stats/Angle.tsx
import { jsx as jsx127 } from "react/jsx-runtime";
var STEP_SIZE2 = 15;
var handleDegreeChange = ({
accumulatedChange,
originalElements,
shouldChangeByStepSize,
nextValue,
scene
}) => {
const elementsMap = scene.getNonDeletedElementsMap();
const elements = scene.getNonDeletedElements();
const origElement = originalElements[0];
if (origElement && !isElbowArrow(origElement)) {
const latestElement = elementsMap.get(origElement.id);
if (!latestElement) {
return;
}
if (nextValue !== void 0) {
const nextAngle2 = degreesToRadians(nextValue);
mutateElement(latestElement, {
angle: nextAngle2
});
updateBindings(latestElement, elementsMap, elements, scene);
const boundTextElement2 = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement2 && !isArrowElement(latestElement)) {
mutateElement(boundTextElement2, { angle: nextAngle2 });
}
return;
}
const originalAngleInDegrees = Math.round(radiansToDegrees(origElement.angle) * 100) / 100;
const changeInDegrees = Math.round(accumulatedChange);
let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
if (shouldChangeByStepSize) {
nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE2);
}
nextAngleInDegrees = nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
const nextAngle = degreesToRadians(nextAngleInDegrees);
mutateElement(latestElement, {
angle: nextAngle
});
updateBindings(latestElement, elementsMap, elements, scene);
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) {
mutateElement(boundTextElement, { angle: nextAngle });
}
}
};
var Angle = ({ element, scene, appState, property }) => {
return /* @__PURE__ */ jsx127(
DragInput_default,
{
label: "A",
icon: angleIcon,
value: Math.round(radiansToDegrees(element.angle) % 360 * 100) / 100,
elements: [element],
dragInputCallback: handleDegreeChange,
editable: isPropertyEditable(element, "angle"),
scene,
appState,
property
}
);
};
var Angle_default = Angle;
// components/Stats/FontSize.tsx
import { jsx as jsx128 } from "react/jsx-runtime";
var MIN_FONT_SIZE = 4;
var STEP_SIZE3 = 4;
var handleFontSizeChange = ({
accumulatedChange,
originalElements,
shouldChangeByStepSize,
nextValue,
scene
}) => {
const elementsMap = scene.getNonDeletedElementsMap();
const origElement = originalElements[0];
if (origElement) {
const latestElement = elementsMap.get(origElement.id);
if (!latestElement || !isTextElement(latestElement)) {
return;
}
let nextFontSize;
if (nextValue !== void 0) {
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE);
} else if (origElement.type === "text") {
const originalFontSize = Math.round(origElement.fontSize);
const changeInFontSize = Math.round(accumulatedChange);
nextFontSize = Math.max(
originalFontSize + changeInFontSize,
MIN_FONT_SIZE
);
if (shouldChangeByStepSize) {
nextFontSize = getStepSizedValue(nextFontSize, STEP_SIZE3);
}
}
if (nextFontSize) {
mutateElement(latestElement, {
fontSize: nextFontSize
});
redrawTextBoundingBox(
latestElement,
scene.getContainerElement(latestElement),
scene.getNonDeletedElementsMap()
);
}
}
};
var FontSize = ({ element, scene, appState, property }) => {
const _element = isTextElement(element) ? element : hasBoundTextElement(element) ? getBoundTextElement(element, scene.getNonDeletedElementsMap()) : null;
if (!_element) {
return null;
}
return /* @__PURE__ */ jsx128(
DragInput_default,
{
label: "F",
value: Math.round(_element.fontSize * 10) / 10,
elements: [_element],
dragInputCallback: handleFontSizeChange,
icon: fontSizeIcon,
appState,
scene,
property
}
);
};
var FontSize_default = FontSize;
// components/Stats/MultiDimension.tsx
import { useMemo as useMemo7 } from "react";
import { jsx as jsx129 } from "react/jsx-runtime";
var STEP_SIZE4 = 10;
var getResizedUpdates = (anchorX, anchorY, scale, origElement) => {
const offsetX = origElement.x - anchorX;
const offsetY = origElement.y - anchorY;
const nextWidth = origElement.width * scale;
const nextHeight = origElement.height * scale;
const x = anchorX + offsetX * scale;
const y = anchorY + offsetY * scale;
return {
width: nextWidth,
height: nextHeight,
x,
y,
...rescalePointsInElement(origElement, nextWidth, nextHeight, false),
...isTextElement(origElement) ? { fontSize: origElement.fontSize * scale } : {}
};
};
var resizeElementInGroup = (anchorX, anchorY, property, scale, latestElement, origElement, elementsMap, originalElementsMap) => {
const updates = getResizedUpdates(anchorX, anchorY, scale, origElement);
mutateElement(latestElement, updates, false);
const boundTextElement = getBoundTextElement(
origElement,
originalElementsMap
);
if (boundTextElement) {
const newFontSize = boundTextElement.fontSize * scale;
updateBoundElements(latestElement, elementsMap, {
newSize: { width: updates.width, height: updates.height }
});
const latestBoundTextElement = elementsMap.get(boundTextElement.id);
if (latestBoundTextElement && isTextElement(latestBoundTextElement)) {
mutateElement(
latestBoundTextElement,
{
fontSize: newFontSize
},
false
);
handleBindTextResize(
latestElement,
elementsMap,
property === "width" ? "e" : "s",
true
);
}
}
};
var resizeGroup = (nextWidth, nextHeight, initialHeight, aspectRatio, anchor, property, latestElements, originalElements, elementsMap, originalElementsMap) => {
if (property === "width") {
nextHeight = Math.round(nextWidth / aspectRatio * 100) / 100;
} else {
nextWidth = Math.round(nextHeight * aspectRatio * 100) / 100;
}
const scale = nextHeight / initialHeight;
for (let i = 0; i < originalElements.length; i++) {
const origElement = originalElements[i];
const latestElement = latestElements[i];
resizeElementInGroup(
anchor[0],
anchor[1],
property,
scale,
latestElement,
origElement,
elementsMap,
originalElementsMap
);
}
};
var handleDimensionChange2 = ({
accumulatedChange,
originalElements,
originalElementsMap,
originalAppState,
shouldChangeByStepSize,
nextValue,
scene,
property
}) => {
const elementsMap = scene.getNonDeletedElementsMap();
const atomicUnits = getAtomicUnits(originalElements, originalAppState);
if (nextValue !== void 0) {
for (const atomicUnit of atomicUnits) {
const elementsInUnit = getElementsInAtomicUnit(
atomicUnit,
elementsMap,
originalElementsMap
);
if (elementsInUnit.length > 1) {
const latestElements = elementsInUnit.map((el) => el.latest);
const originalElements2 = elementsInUnit.map((el) => el.original);
const [x1, y1, x2, y2] = getCommonBounds(originalElements2);
const initialWidth = x2 - x1;
const initialHeight = y2 - y1;
const aspectRatio = initialWidth / initialHeight;
const nextWidth = Math.max(
MIN_WIDTH_OR_HEIGHT,
property === "width" ? Math.max(0, nextValue) : initialWidth
);
const nextHeight = Math.max(
MIN_WIDTH_OR_HEIGHT,
property === "height" ? Math.max(0, nextValue) : initialHeight
);
resizeGroup(
nextWidth,
nextHeight,
initialHeight,
aspectRatio,
pointFrom(x1, y1),
property,
latestElements,
originalElements2,
elementsMap,
originalElementsMap
);
} else {
const [el] = elementsInUnit;
const latestElement = el?.latest;
const origElement = el?.original;
if (latestElement && origElement && isPropertyEditable(latestElement, property)) {
let nextWidth = property === "width" ? Math.max(0, nextValue) : latestElement.width;
if (property === "width") {
if (shouldChangeByStepSize) {
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE4);
} else {
nextWidth = Math.round(nextWidth);
}
}
let nextHeight = property === "height" ? Math.max(0, nextValue) : latestElement.height;
if (property === "height") {
if (shouldChangeByStepSize) {
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE4);
} else {
nextHeight = Math.round(nextHeight);
}
}
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
resizeSingleElement(
nextWidth,
nextHeight,
latestElement,
origElement,
elementsMap,
originalElementsMap,
property === "width" ? "e" : "s",
{
shouldInformMutation: false
}
);
}
}
}
scene.triggerUpdate();
return;
}
const changeInWidth = property === "width" ? accumulatedChange : 0;
const changeInHeight = property === "height" ? accumulatedChange : 0;
for (const atomicUnit of atomicUnits) {
const elementsInUnit = getElementsInAtomicUnit(
atomicUnit,
elementsMap,
originalElementsMap
);
if (elementsInUnit.length > 1) {
const latestElements = elementsInUnit.map((el) => el.latest);
const originalElements2 = elementsInUnit.map((el) => el.original);
const [x1, y1, x2, y2] = getCommonBounds(originalElements2);
const initialWidth = x2 - x1;
const initialHeight = y2 - y1;
const aspectRatio = initialWidth / initialHeight;
let nextWidth = Math.max(0, initialWidth + changeInWidth);
if (property === "width") {
if (shouldChangeByStepSize) {
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE4);
} else {
nextWidth = Math.round(nextWidth);
}
}
let nextHeight = Math.max(0, initialHeight + changeInHeight);
if (property === "height") {
if (shouldChangeByStepSize) {
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE4);
} else {
nextHeight = Math.round(nextHeight);
}
}
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
resizeGroup(
nextWidth,
nextHeight,
initialHeight,
aspectRatio,
pointFrom(x1, y1),
property,
latestElements,
originalElements2,
elementsMap,
originalElementsMap
);
} else {
const [el] = elementsInUnit;
const latestElement = el?.latest;
const origElement = el?.original;
if (latestElement && origElement && isPropertyEditable(latestElement, property)) {
let nextWidth = Math.max(0, origElement.width + changeInWidth);
if (property === "width") {
if (shouldChangeByStepSize) {
nextWidth = getStepSizedValue(nextWidth, STEP_SIZE4);
} else {
nextWidth = Math.round(nextWidth);
}
}
let nextHeight = Math.max(0, origElement.height + changeInHeight);
if (property === "height") {
if (shouldChangeByStepSize) {
nextHeight = getStepSizedValue(nextHeight, STEP_SIZE4);
} else {
nextHeight = Math.round(nextHeight);
}
}
nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth);
nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight);
resizeSingleElement(
nextWidth,
nextHeight,
latestElement,
origElement,
elementsMap,
originalElementsMap,
property === "width" ? "e" : "s",
{
shouldInformMutation: false
}
);
}
}
}
scene.triggerUpdate();
};
var MultiDimension = ({
property,
elements,
elementsMap,
atomicUnits,
scene,
appState
}) => {
const sizes = useMemo7(
() => atomicUnits.map((atomicUnit) => {
const elementsInUnit = getElementsInAtomicUnit(atomicUnit, elementsMap);
if (elementsInUnit.length > 1) {
const [x1, y1, x2, y2] = getCommonBounds(
elementsInUnit.map((el2) => el2.latest)
);
return Math.round((property === "width" ? x2 - x1 : y2 - y1) * 100) / 100;
}
const [el] = elementsInUnit;
return Math.round(
(property === "width" ? el.latest.width : el.latest.height) * 100
) / 100;
}),
[elementsMap, atomicUnits, property]
);
const value = new Set(sizes).size === 1 ? Math.round(sizes[0] * 100) / 100 : "Mixed";
const editable = sizes.length > 0;
return /* @__PURE__ */ jsx129(
DragInput_default,
{
label: property === "width" ? "W" : "H",
elements,
dragInputCallback: handleDimensionChange2,
value,
editable,
appState,
property,
scene
}
);
};
var MultiDimension_default = MultiDimension;
// components/Stats/MultiAngle.tsx
import { jsx as jsx130 } from "react/jsx-runtime";
var STEP_SIZE5 = 15;
var handleDegreeChange2 = ({
accumulatedChange,
originalElements,
shouldChangeByStepSize,
nextValue,
property,
scene
}) => {
const elementsMap = scene.getNonDeletedElementsMap();
const editableLatestIndividualElements = originalElements.map((el) => elementsMap.get(el.id)).filter((el) => el && !isInGroup(el) && isPropertyEditable(el, property));
const editableOriginalIndividualElements = originalElements.filter(
(el) => !isInGroup(el) && isPropertyEditable(el, property)
);
if (nextValue !== void 0) {
const nextAngle = degreesToRadians(nextValue);
for (const element of editableLatestIndividualElements) {
if (!element) {
continue;
}
mutateElement(
element,
{
angle: nextAngle
},
false
);
const boundTextElement = getBoundTextElement(element, elementsMap);
if (boundTextElement && !isArrowElement(element)) {
mutateElement(boundTextElement, { angle: nextAngle }, false);
}
}
scene.triggerUpdate();
return;
}
for (let i = 0; i < editableLatestIndividualElements.length; i++) {
const latestElement = editableLatestIndividualElements[i];
if (!latestElement) {
continue;
}
const originalElement = editableOriginalIndividualElements[i];
const originalAngleInDegrees = Math.round(radiansToDegrees(originalElement.angle) * 100) / 100;
const changeInDegrees = Math.round(accumulatedChange);
let nextAngleInDegrees = (originalAngleInDegrees + changeInDegrees) % 360;
if (shouldChangeByStepSize) {
nextAngleInDegrees = getStepSizedValue(nextAngleInDegrees, STEP_SIZE5);
}
nextAngleInDegrees = nextAngleInDegrees < 0 ? nextAngleInDegrees + 360 : nextAngleInDegrees;
const nextAngle = degreesToRadians(nextAngleInDegrees);
mutateElement(
latestElement,
{
angle: nextAngle
},
false
);
const boundTextElement = getBoundTextElement(latestElement, elementsMap);
if (boundTextElement && !isArrowElement(latestElement)) {
mutateElement(boundTextElement, { angle: nextAngle }, false);
}
}
scene.triggerUpdate();
};
var MultiAngle = ({
elements,
scene,
appState,
property
}) => {
const editableLatestIndividualElements = elements.filter(
(el) => !isInGroup(el) && isPropertyEditable(el, "angle")
);
const angles = editableLatestIndividualElements.map(
(el) => Math.round(radiansToDegrees(el.angle) % 360 * 100) / 100
);
const value = new Set(angles).size === 1 ? angles[0] : "Mixed";
const editable = editableLatestIndividualElements.some(
(el) => isPropertyEditable(el, "angle")
);
return /* @__PURE__ */ jsx130(
DragInput_default,
{
label: "A",
icon: angleIcon,
value,
elements,
dragInputCallback: handleDegreeChange2,
editable,
appState,
scene,
property
}
);
};
var MultiAngle_default = MultiAngle;
// components/Stats/MultiFontSize.tsx
import { jsx as jsx131 } from "react/jsx-runtime";
var MIN_FONT_SIZE2 = 4;
var STEP_SIZE6 = 4;
var getApplicableTextElements = (elements, elementsMap) => elements.reduce(
(acc, el) => {
if (!el || isInGroup(el)) {
return acc;
}
if (isTextElement(el)) {
acc.push(el);
return acc;
}
if (hasBoundTextElement(el)) {
const boundTextElement = getBoundTextElement(el, elementsMap);
if (boundTextElement) {
acc.push(boundTextElement);
return acc;
}
}
return acc;
},
[]
);
var handleFontSizeChange2 = ({
accumulatedChange,
originalElements,
shouldChangeByStepSize,
nextValue,
scene
}) => {
const elementsMap = scene.getNonDeletedElementsMap();
const latestTextElements = originalElements.map(
(el) => elementsMap.get(el.id)
);
let nextFontSize;
if (nextValue) {
nextFontSize = Math.max(Math.round(nextValue), MIN_FONT_SIZE2);
for (const textElement of latestTextElements) {
mutateElement(
textElement,
{
fontSize: nextFontSize
},
false
);
redrawTextBoundingBox(
textElement,
scene.getContainerElement(textElement),
elementsMap,
false
);
}
scene.triggerUpdate();
} else {
const originalTextElements = originalElements;
for (let i = 0; i < latestTextElements.length; i++) {
const latestElement = latestTextElements[i];
const originalElement = originalTextElements[i];
const originalFontSize = Math.round(originalElement.fontSize);
const changeInFontSize = Math.round(accumulatedChange);
let nextFontSize2 = Math.max(
originalFontSize + changeInFontSize,
MIN_FONT_SIZE2
);
if (shouldChangeByStepSize) {
nextFontSize2 = getStepSizedValue(nextFontSize2, STEP_SIZE6);
}
mutateElement(
latestElement,
{
fontSize: nextFontSize2
},
false
);
redrawTextBoundingBox(
latestElement,
scene.getContainerElement(latestElement),
elementsMap,
false
);
}
scene.triggerUpdate();
}
};
var MultiFontSize = ({
elements,
scene,
appState,
property,
elementsMap
}) => {
const latestTextElements = getApplicableTextElements(elements, elementsMap);
if (!latestTextElements.length) {
return null;
}
const fontSizes = latestTextElements.map(
(textEl) => Math.round(textEl.fontSize * 10) / 10
);
const value = new Set(fontSizes).size === 1 ? fontSizes[0] : "Mixed";
const editable = fontSizes.length > 0;
return /* @__PURE__ */ jsx131(
DragInput_default,
{
label: "F",
icon: fontSizeIcon,
elements: latestTextElements,
dragInputCallback: handleFontSizeChange2,
value,
editable,
scene,
property,
appState
}
);
};
var MultiFontSize_default = MultiFontSize;
// components/Stats/Position.tsx
import { jsx as jsx132 } from "react/jsx-runtime";
var STEP_SIZE7 = 10;
var handlePositionChange = ({
accumulatedChange,
instantChange,
originalElements,
originalElementsMap,
shouldChangeByStepSize,
nextValue,
property,
scene,
originalAppState
}) => {
const elementsMap = scene.getNonDeletedElementsMap();
const elements = scene.getNonDeletedElements();
const origElement = originalElements[0];
const [cx, cy] = [
origElement.x + origElement.width / 2,
origElement.y + origElement.height / 2
];
const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(origElement.x, origElement.y),
pointFrom(cx, cy),
origElement.angle
);
if (originalAppState.croppingElementId === origElement.id) {
const element = elementsMap.get(origElement.id);
if (!element || !isImageElement(element) || !element.crop) {
return;
}
const crop = element.crop;
let nextCrop = crop;
const isFlippedByX = element.scale[0] === -1;
const isFlippedByY = element.scale[1] === -1;
const { width: uncroppedWidth, height: uncroppedHeight } = getUncroppedWidthAndHeight(element);
if (nextValue !== void 0) {
if (property === "x") {
const nextValueInNatural = nextValue * (crop.naturalWidth / uncroppedWidth);
if (isFlippedByX) {
nextCrop = {
...crop,
x: clamp(
crop.naturalWidth - nextValueInNatural - crop.width,
0,
crop.naturalWidth - crop.width
)
};
} else {
nextCrop = {
...crop,
x: clamp(
nextValue * (crop.naturalWidth / uncroppedWidth),
0,
crop.naturalWidth - crop.width
)
};
}
}
if (property === "y") {
nextCrop = {
...crop,
y: clamp(
nextValue * (crop.naturalHeight / uncroppedHeight),
0,
crop.naturalHeight - crop.height
)
};
}
mutateElement(element, {
crop: nextCrop
});
return;
}
const changeInX = (property === "x" ? instantChange : 0) * (isFlippedByX ? -1 : 1);
const changeInY = (property === "y" ? instantChange : 0) * (isFlippedByY ? -1 : 1);
nextCrop = {
...crop,
x: clamp(crop.x + changeInX, 0, crop.naturalWidth - crop.width),
y: clamp(crop.y + changeInY, 0, crop.naturalHeight - crop.height)
};
mutateElement(element, {
crop: nextCrop
});
return;
}
if (nextValue !== void 0) {
const newTopLeftX2 = property === "x" ? nextValue : topLeftX;
const newTopLeftY2 = property === "y" ? nextValue : topLeftY;
moveElement(
newTopLeftX2,
newTopLeftY2,
origElement,
elementsMap,
elements,
scene,
originalElementsMap
);
return;
}
const changeInTopX = property === "x" ? accumulatedChange : 0;
const changeInTopY = property === "y" ? accumulatedChange : 0;
const newTopLeftX = property === "x" ? Math.round(
shouldChangeByStepSize ? getStepSizedValue(origElement.x + changeInTopX, STEP_SIZE7) : topLeftX + changeInTopX
) : topLeftX;
const newTopLeftY = property === "y" ? Math.round(
shouldChangeByStepSize ? getStepSizedValue(origElement.y + changeInTopY, STEP_SIZE7) : topLeftY + changeInTopY
) : topLeftY;
moveElement(
newTopLeftX,
newTopLeftY,
origElement,
elementsMap,
elements,
scene,
originalElementsMap
);
};
var Position = ({
property,
element,
elementsMap,
scene,
appState
}) => {
const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(element.x, element.y),
pointFrom(element.x + element.width / 2, element.y + element.height / 2),
element.angle
);
let value = round(property === "x" ? topLeftX : topLeftY, 2);
if (appState.croppingElementId === element.id && isImageElement(element) && element.crop) {
const flipAdjustedPosition = getFlipAdjustedCropPosition(element);
if (flipAdjustedPosition) {
value = round(
property === "x" ? flipAdjustedPosition.x : flipAdjustedPosition.y,
2
);
}
}
return /* @__PURE__ */ jsx132(
DragInput_default,
{
label: property === "x" ? "X" : "Y",
elements: [element],
dragInputCallback: handlePositionChange,
scene,
value,
property,
appState
}
);
};
var Position_default = Position;
// components/Stats/MultiPosition.tsx
import { useMemo as useMemo8 } from "react";
import { jsx as jsx133 } from "react/jsx-runtime";
var STEP_SIZE8 = 10;
var moveElements = (property, changeInTopX, changeInTopY, elements, originalElements, elementsMap, originalElementsMap, scene) => {
for (let i = 0; i < elements.length; i++) {
const origElement = originalElements[i];
const [cx, cy] = [
origElement.x + origElement.width / 2,
origElement.y + origElement.height / 2
];
const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(origElement.x, origElement.y),
pointFrom(cx, cy),
origElement.angle
);
const newTopLeftX = property === "x" ? Math.round(topLeftX + changeInTopX) : topLeftX;
const newTopLeftY = property === "y" ? Math.round(topLeftY + changeInTopY) : topLeftY;
moveElement(
newTopLeftX,
newTopLeftY,
origElement,
elementsMap,
elements,
scene,
originalElementsMap,
false
);
}
};
var moveGroupTo = (nextX, nextY, originalElements, elementsMap, elements, originalElementsMap, scene) => {
const [x1, y1, ,] = getCommonBounds(originalElements);
const offsetX = nextX - x1;
const offsetY = nextY - y1;
for (let i = 0; i < originalElements.length; i++) {
const origElement = originalElements[i];
const latestElement = elementsMap.get(origElement.id);
if (!latestElement) {
continue;
}
if (!isTextElement(latestElement) || !latestElement.containerId) {
const [cx, cy] = [
latestElement.x + latestElement.width / 2,
latestElement.y + latestElement.height / 2
];
const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(latestElement.x, latestElement.y),
pointFrom(cx, cy),
latestElement.angle
);
moveElement(
topLeftX + offsetX,
topLeftY + offsetY,
origElement,
elementsMap,
elements,
scene,
originalElementsMap,
false
);
}
}
};
var handlePositionChange2 = ({
accumulatedChange,
originalElements,
originalElementsMap,
shouldChangeByStepSize,
nextValue,
property,
scene,
originalAppState
}) => {
const elementsMap = scene.getNonDeletedElementsMap();
const elements = scene.getNonDeletedElements();
if (nextValue !== void 0) {
for (const atomicUnit of getAtomicUnits(
originalElements,
originalAppState
)) {
const elementsInUnit = getElementsInAtomicUnit(
atomicUnit,
elementsMap,
originalElementsMap
);
if (elementsInUnit.length > 1) {
const [x1, y1, ,] = getCommonBounds(
elementsInUnit.map((el) => el.latest)
);
const newTopLeftX = property === "x" ? nextValue : x1;
const newTopLeftY = property === "y" ? nextValue : y1;
moveGroupTo(
newTopLeftX,
newTopLeftY,
elementsInUnit.map((el) => el.original),
elementsMap,
elements,
originalElementsMap,
scene
);
} else {
const origElement = elementsInUnit[0]?.original;
const latestElement = elementsInUnit[0]?.latest;
if (origElement && latestElement && isPropertyEditable(latestElement, property)) {
const [cx, cy] = [
origElement.x + origElement.width / 2,
origElement.y + origElement.height / 2
];
const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(origElement.x, origElement.y),
pointFrom(cx, cy),
origElement.angle
);
const newTopLeftX = property === "x" ? nextValue : topLeftX;
const newTopLeftY = property === "y" ? nextValue : topLeftY;
moveElement(
newTopLeftX,
newTopLeftY,
origElement,
elementsMap,
elements,
scene,
originalElementsMap,
false
);
}
}
}
scene.triggerUpdate();
return;
}
const change = shouldChangeByStepSize ? getStepSizedValue(accumulatedChange, STEP_SIZE8) : accumulatedChange;
const changeInTopX = property === "x" ? change : 0;
const changeInTopY = property === "y" ? change : 0;
moveElements(
property,
changeInTopX,
changeInTopY,
originalElements,
originalElements,
elementsMap,
originalElementsMap,
scene
);
scene.triggerUpdate();
};
var MultiPosition = ({
property,
elements,
elementsMap,
atomicUnits,
scene,
appState
}) => {
const positions = useMemo8(
() => atomicUnits.map((atomicUnit) => {
const elementsInUnit = Object.keys(atomicUnit).map((id) => elementsMap.get(id)).filter((el2) => el2 !== void 0);
if (elementsInUnit.length > 1) {
const [x1, y1] = getCommonBounds(elementsInUnit);
return Math.round((property === "x" ? x1 : y1) * 100) / 100;
}
const [el] = elementsInUnit;
const [cx, cy] = [el.x + el.width / 2, el.y + el.height / 2];
const [topLeftX, topLeftY] = pointRotateRads(
pointFrom(el.x, el.y),
pointFrom(cx, cy),
el.angle
);
return Math.round((property === "x" ? topLeftX : topLeftY) * 100) / 100;
}),
[atomicUnits, elementsMap, property]
);
const value = new Set(positions).size === 1 ? positions[0] : "Mixed";
return /* @__PURE__ */ jsx133(
DragInput_default,
{
label: property === "x" ? "X" : "Y",
elements,
dragInputCallback: handlePositionChange2,
value,
property,
scene,
appState
}
);
};
var MultiPosition_default = MultiPosition;
// components/Stats/CanvasGrid.tsx
import { jsx as jsx134 } from "react/jsx-runtime";
var STEP_SIZE9 = 5;
var CanvasGrid = ({
property,
scene,
appState,
setAppState
}) => {
return /* @__PURE__ */ jsx134(
DragInput_default,
{
label: "Grid step",
sensitivity: 8,
elements: [],
dragInputCallback: ({
nextValue,
instantChange,
shouldChangeByStepSize,
setInputValue
}) => {
setAppState((state) => {
let nextGridStep;
if (nextValue) {
nextGridStep = nextValue;
} else if (instantChange) {
nextGridStep = shouldChangeByStepSize ? getStepSizedValue(
state.gridStep + STEP_SIZE9 * Math.sign(instantChange),
STEP_SIZE9
) : state.gridStep + instantChange;
}
if (!nextGridStep) {
setInputValue(state.gridStep);
return null;
}
nextGridStep = getNormalizedGridStep(nextGridStep);
setInputValue(nextGridStep);
return {
gridStep: nextGridStep
};
});
},
scene,
value: appState.gridStep,
property,
appState
}
);
};
var CanvasGrid_default = CanvasGrid;
// components/Stats/index.tsx
import clsx52 from "clsx";
import { Fragment as Fragment21, jsx as jsx135, jsxs as jsxs71 } from "react/jsx-runtime";
var STATS_TIMEOUT = 50;
var Stats = (props) => {
const appState = useExcalidrawAppState();
const sceneNonce = props.app.scene.getSceneNonce() || 1;
const selectedElements = props.app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: false
});
const gridModeEnabled = isGridModeEnabled(props.app);
return /* @__PURE__ */ jsx135(
StatsInner,
{
...props,
appState,
sceneNonce,
selectedElements,
gridModeEnabled
}
);
};
var StatsRow = ({
children,
columns = 1,
heading,
style,
...rest
}) => /* @__PURE__ */ jsx135(
"div",
{
className: clsx52("exc-stats__row", { "exc-stats__row--heading": heading }),
style: {
gridTemplateColumns: `repeat(${columns}, 1fr)`,
...style
},
...rest,
children
}
);
StatsRow.displayName = "StatsRow";
var StatsRows = ({
children,
order,
style,
...rest
}) => /* @__PURE__ */ jsx135("div", { className: "exc-stats__rows", style: { order, ...style }, ...rest, children });
StatsRows.displayName = "StatsRows";
Stats.StatsRow = StatsRow;
Stats.StatsRows = StatsRows;
var StatsInner = memo5(
({
app,
onClose,
renderCustomStats,
selectedElements,
appState,
sceneNonce,
gridModeEnabled
}) => {
const scene = app.scene;
const elements = scene.getNonDeletedElements();
const elementsMap = scene.getNonDeletedElementsMap();
const setAppState = useExcalidrawSetAppState();
const singleElement = selectedElements.length === 1 ? selectedElements[0] : null;
const multipleElements = selectedElements.length > 1 ? selectedElements : null;
const cropMode = appState.croppingElementId && isImageElement(singleElement);
const unCroppedDimension = cropMode ? getUncroppedWidthAndHeight(singleElement) : null;
const [sceneDimension, setSceneDimension] = useState35({
width: 0,
height: 0
});
const throttledSetSceneDimension = useMemo9(
() => throttle2((elements2) => {
const boundingBox = getCommonBounds(elements2);
setSceneDimension({
width: Math.round(boundingBox[2]) - Math.round(boundingBox[0]),
height: Math.round(boundingBox[3]) - Math.round(boundingBox[1])
});
}, STATS_TIMEOUT),
[]
);
useEffect38(() => {
throttledSetSceneDimension(elements);
}, [sceneNonce, elements, throttledSetSceneDimension]);
useEffect38(
() => () => throttledSetSceneDimension.cancel(),
[throttledSetSceneDimension]
);
const atomicUnits = useMemo9(() => {
return getAtomicUnits(selectedElements, appState);
}, [selectedElements, appState]);
const _frameAndChildrenSelectedTogether = useMemo9(() => {
return frameAndChildrenSelectedTogether(selectedElements);
}, [selectedElements]);
return /* @__PURE__ */ jsx135("div", { className: "exc-stats", children: /* @__PURE__ */ jsxs71(Island, { padding: 3, children: [
/* @__PURE__ */ jsxs71("div", { className: "title", children: [
/* @__PURE__ */ jsx135("h2", { children: t("stats.title") }),
/* @__PURE__ */ jsx135("div", { className: "close", onClick: onClose, children: CloseIcon })
] }),
/* @__PURE__ */ jsxs71(
Collapsible_default,
{
label: /* @__PURE__ */ jsx135("h3", { children: t("stats.generalStats") }),
open: !!(appState.stats.panels & STATS_PANELS.generalStats),
openTrigger: () => setAppState((state) => {
return {
stats: {
open: true,
panels: state.stats.panels ^ STATS_PANELS.generalStats
}
};
}),
children: [
/* @__PURE__ */ jsxs71(StatsRows, { children: [
/* @__PURE__ */ jsx135(StatsRow, { heading: true, children: t("stats.scene") }),
/* @__PURE__ */ jsxs71(StatsRow, { columns: 2, children: [
/* @__PURE__ */ jsx135("div", { children: t("stats.shapes") }),
/* @__PURE__ */ jsx135("div", { children: elements.length })
] }),
/* @__PURE__ */ jsxs71(StatsRow, { columns: 2, children: [
/* @__PURE__ */ jsx135("div", { children: t("stats.width") }),
/* @__PURE__ */ jsx135("div", { children: sceneDimension.width })
] }),
/* @__PURE__ */ jsxs71(StatsRow, { columns: 2, children: [
/* @__PURE__ */ jsx135("div", { children: t("stats.height") }),
/* @__PURE__ */ jsx135("div", { children: sceneDimension.height })
] }),
gridModeEnabled && /* @__PURE__ */ jsxs71(Fragment21, { children: [
/* @__PURE__ */ jsx135(StatsRow, { heading: true, children: "Canvas" }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
CanvasGrid_default,
{
property: "gridStep",
scene,
appState,
setAppState
}
) })
] })
] }),
renderCustomStats?.(elements, appState)
]
}
),
!_frameAndChildrenSelectedTogether && selectedElements.length > 0 && /* @__PURE__ */ jsx135(
"div",
{
id: "elementStats",
style: {
marginTop: 12
},
children: /* @__PURE__ */ jsx135(
Collapsible_default,
{
label: /* @__PURE__ */ jsx135("h3", { children: t("stats.elementProperties") }),
open: !!(appState.stats.panels & STATS_PANELS.elementProperties),
openTrigger: () => setAppState((state) => {
return {
stats: {
open: true,
panels: state.stats.panels ^ STATS_PANELS.elementProperties
}
};
}),
children: /* @__PURE__ */ jsxs71(StatsRows, { children: [
singleElement && /* @__PURE__ */ jsxs71(Fragment21, { children: [
cropMode && /* @__PURE__ */ jsx135(StatsRow, { heading: true, children: t("labels.unCroppedDimension") }),
appState.croppingElementId && isImageElement(singleElement) && unCroppedDimension && /* @__PURE__ */ jsxs71(StatsRow, { columns: 2, children: [
/* @__PURE__ */ jsx135("div", { children: t("stats.width") }),
/* @__PURE__ */ jsx135("div", { children: round(unCroppedDimension.width, 2) })
] }),
appState.croppingElementId && isImageElement(singleElement) && unCroppedDimension && /* @__PURE__ */ jsxs71(StatsRow, { columns: 2, children: [
/* @__PURE__ */ jsx135("div", { children: t("stats.height") }),
/* @__PURE__ */ jsx135("div", { children: round(unCroppedDimension.height, 2) })
] }),
/* @__PURE__ */ jsx135(StatsRow, { heading: true, "data-testid": "stats-element-type", children: appState.croppingElementId ? t("labels.imageCropping") : t(`element.${singleElement.type}`) }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
Position_default,
{
element: singleElement,
property: "x",
elementsMap,
scene,
appState
}
) }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
Position_default,
{
element: singleElement,
property: "y",
elementsMap,
scene,
appState
}
) }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
Dimension_default,
{
property: "width",
element: singleElement,
scene,
appState
}
) }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
Dimension_default,
{
property: "height",
element: singleElement,
scene,
appState
}
) }),
!isElbowArrow(singleElement) && /* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
Angle_default,
{
property: "angle",
element: singleElement,
scene,
appState
}
) }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
FontSize_default,
{
property: "fontSize",
element: singleElement,
scene,
appState
}
) })
] }),
multipleElements && /* @__PURE__ */ jsxs71(Fragment21, { children: [
elementsAreInSameGroup(multipleElements) && /* @__PURE__ */ jsx135(StatsRow, { heading: true, children: t("element.group") }),
/* @__PURE__ */ jsxs71(StatsRow, { columns: 2, style: { margin: "0.3125rem 0" }, children: [
/* @__PURE__ */ jsx135("div", { children: t("stats.shapes") }),
/* @__PURE__ */ jsx135("div", { children: selectedElements.length })
] }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
MultiPosition_default,
{
property: "x",
elements: multipleElements,
elementsMap,
atomicUnits,
scene,
appState
}
) }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
MultiPosition_default,
{
property: "y",
elements: multipleElements,
elementsMap,
atomicUnits,
scene,
appState
}
) }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
MultiDimension_default,
{
property: "width",
elements: multipleElements,
elementsMap,
atomicUnits,
scene,
appState
}
) }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
MultiDimension_default,
{
property: "height",
elements: multipleElements,
elementsMap,
atomicUnits,
scene,
appState
}
) }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
MultiAngle_default,
{
property: "angle",
elements: multipleElements,
scene,
appState
}
) }),
/* @__PURE__ */ jsx135(StatsRow, { children: /* @__PURE__ */ jsx135(
MultiFontSize_default,
{
property: "fontSize",
elements: multipleElements,
scene,
appState,
elementsMap
}
) })
] })
] })
}
)
}
)
] }) });
},
(prev, next) => {
return prev.sceneNonce === next.sceneNonce && prev.selectedElements === next.selectedElements && prev.appState.stats.panels === next.appState.stats.panels && prev.gridModeEnabled === next.gridModeEnabled && prev.appState.gridStep === next.appState.gridStep && prev.appState.croppingElementId === next.appState.croppingElementId;
}
);
// components/ElementLinkDialog.tsx
import { useCallback as useCallback13, useEffect as useEffect39, useState as useState36 } from "react";
import { jsx as jsx136, jsxs as jsxs72 } from "react/jsx-runtime";
var ElementLinkDialog = ({
sourceElementId,
onClose,
elementsMap,
appState,
generateLinkForSelection = defaultGetElementLinkFromSelection
}) => {
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
const [nextLink, setNextLink] = useState36(originalLink);
const [linkEdited, setLinkEdited] = useState36(false);
useEffect39(() => {
const selectedElements = getSelectedElements(elementsMap, appState);
let nextLink2 = originalLink;
if (selectedElements.length > 0 && generateLinkForSelection) {
const idAndType = getLinkIdAndTypeFromSelection(
selectedElements,
appState
);
if (idAndType) {
nextLink2 = normalizeLink(
generateLinkForSelection(idAndType.id, idAndType.type)
);
}
}
setNextLink(nextLink2);
}, [
elementsMap,
appState,
appState.selectedElementIds,
originalLink,
generateLinkForSelection
]);
const handleConfirm = useCallback13(() => {
if (nextLink && nextLink !== elementsMap.get(sourceElementId)?.link) {
const elementToLink = elementsMap.get(sourceElementId);
elementToLink && mutateElement(elementToLink, {
link: nextLink
});
}
if (!nextLink && linkEdited && sourceElementId) {
const elementToLink = elementsMap.get(sourceElementId);
elementToLink && mutateElement(elementToLink, {
link: null
});
}
onClose?.();
}, [sourceElementId, nextLink, elementsMap, linkEdited, onClose]);
useEffect39(() => {
const handleKeyDown = (event) => {
if (appState.openDialog?.name === "elementLinkSelector" && event.key === KEYS.ENTER) {
handleConfirm();
}
if (appState.openDialog?.name === "elementLinkSelector" && event.key === KEYS.ESCAPE) {
onClose?.();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [appState, onClose, handleConfirm]);
return /* @__PURE__ */ jsxs72("div", { className: "ElementLinkDialog", children: [
/* @__PURE__ */ jsxs72("div", { className: "ElementLinkDialog__header", children: [
/* @__PURE__ */ jsx136("h2", { children: t("elementLink.title") }),
/* @__PURE__ */ jsx136("p", { children: t("elementLink.desc") })
] }),
/* @__PURE__ */ jsxs72("div", { className: "ElementLinkDialog__input", children: [
/* @__PURE__ */ jsx136(
TextField,
{
value: nextLink ?? "",
onChange: (value) => {
if (!linkEdited) {
setLinkEdited(true);
}
setNextLink(value);
},
onKeyDown: (event) => {
if (event.key === KEYS.ENTER) {
handleConfirm();
}
},
className: "ElementLinkDialog__input-field",
selectOnRender: true
}
),
originalLink && nextLink && /* @__PURE__ */ jsx136(
ToolButton,
{
type: "button",
title: t("buttons.remove"),
"aria-label": t("buttons.remove"),
label: t("buttons.remove"),
onClick: () => {
setNextLink(null);
setLinkEdited(true);
},
className: "ElementLinkDialog__remove",
icon: TrashIcon
}
)
] }),
/* @__PURE__ */ jsxs72("div", { className: "ElementLinkDialog__actions", children: [
/* @__PURE__ */ jsx136(
DialogActionButton_default,
{
label: t("buttons.cancel"),
onClick: () => {
onClose?.();
},
style: {
marginRight: 10
}
}
),
/* @__PURE__ */ jsx136(
DialogActionButton_default,
{
label: t("buttons.confirm"),
onClick: handleConfirm,
actionType: "primary"
}
)
] })
] });
};
var ElementLinkDialog_default = ElementLinkDialog;
// components/LayerUI.tsx
import { Fragment as Fragment22, jsx as jsx137, jsxs as jsxs73 } from "react/jsx-runtime";
var DefaultMainMenu = ({ UIOptions }) => {
return /* @__PURE__ */ jsxs73(MainMenu_default, { __fallback: true, children: [
/* @__PURE__ */ jsx137(MainMenu_default.DefaultItems.LoadScene, {}),
/* @__PURE__ */ jsx137(MainMenu_default.DefaultItems.SaveToActiveFile, {}),
UIOptions.canvasActions.export && /* @__PURE__ */ jsx137(MainMenu_default.DefaultItems.Export, {}),
UIOptions.canvasActions.saveAsImage && /* @__PURE__ */ jsx137(MainMenu_default.DefaultItems.SaveAsImage, {}),
/* @__PURE__ */ jsx137(MainMenu_default.DefaultItems.SearchMenu, {}),
/* @__PURE__ */ jsx137(MainMenu_default.DefaultItems.Help, {}),
/* @__PURE__ */ jsx137(MainMenu_default.DefaultItems.ClearCanvas, {}),
/* @__PURE__ */ jsx137(MainMenu_default.Separator, {}),
/* @__PURE__ */ jsx137(MainMenu_default.Group, { title: "Excalidraw links", children: /* @__PURE__ */ jsx137(MainMenu_default.DefaultItems.Socials, {}) }),
/* @__PURE__ */ jsx137(MainMenu_default.Separator, {}),
/* @__PURE__ */ jsx137(MainMenu_default.DefaultItems.ToggleTheme, {}),
/* @__PURE__ */ jsx137(MainMenu_default.DefaultItems.ChangeCanvasBackground, {})
] });
};
var DefaultOverwriteConfirmDialog = () => {
return /* @__PURE__ */ jsxs73(OverwriteConfirmDialog, { __fallback: true, children: [
/* @__PURE__ */ jsx137(OverwriteConfirmDialog.Actions.SaveToDisk, {}),
/* @__PURE__ */ jsx137(OverwriteConfirmDialog.Actions.ExportToImage, {})
] });
};
var LayerUI = ({
actionManager,
appState,
files,
setAppState,
elements,
canvas,
onLockToggle,
onHandToolToggle,
onPenModeToggle,
showExitZenModeBtn,
renderTopRightUI,
renderCustomStats,
UIOptions,
onExportImage,
renderWelcomeScreen,
children,
app,
isCollaborating,
generateLinkForSelection
}) => {
const device = useDevice();
const tunnels = useInitializeTunnels();
const TunnelsJotaiProvider = tunnels.tunnelsJotai.Provider;
const [eyeDropperState, setEyeDropperState] = useAtom(activeEyeDropperAtom);
const renderJSONExportDialog = () => {
if (!UIOptions.canvasActions.export) {
return null;
}
return /* @__PURE__ */ jsx137(
JSONExportDialog,
{
elements,
appState,
files,
actionManager,
exportOpts: UIOptions.canvasActions.export,
canvas,
setAppState
}
);
};
const renderImageExportDialog = () => {
if (!UIOptions.canvasActions.saveAsImage || appState.openDialog?.name !== "imageExport") {
return null;
}
return /* @__PURE__ */ jsx137(
ImageExportDialog,
{
elements,
appState,
files,
actionManager,
onExportImage,
onCloseRequest: () => setAppState({ openDialog: null }),
name: app.getName()
}
);
};
const renderCanvasActions = () => /* @__PURE__ */ jsxs73("div", { style: { position: "relative" }, children: [
/* @__PURE__ */ jsx137(tunnels.MainMenuTunnel.Out, {}),
renderWelcomeScreen && /* @__PURE__ */ jsx137(tunnels.WelcomeScreenMenuHintTunnel.Out, {})
] });
const renderSelectedShapeActions = () => /* @__PURE__ */ jsx137(
Section,
{
heading: "selectedShapeActions",
className: clsx53("selected-shape-actions zen-mode-transition", {
"transition-left": appState.zenModeEnabled
}),
children: /* @__PURE__ */ jsx137(
Island,
{
className: CLASSES.SHAPE_ACTIONS_MENU,
padding: 2,
style: {
// we want to make sure this doesn't overflow so subtracting the
// approximate height of hamburgerMenu + footer
maxHeight: `${appState.height - 166}px`
},
children: /* @__PURE__ */ jsx137(
SelectedShapeActions,
{
appState,
elementsMap: app.scene.getNonDeletedElementsMap(),
renderAction: actionManager.renderAction,
app
}
)
}
)
}
);
const renderFixedSideContainer = () => {
const shouldRenderSelectedShapeActions = showSelectedShapeActions(
appState,
elements
);
const shouldShowStats = appState.stats.open && !appState.zenModeEnabled && !appState.viewModeEnabled && appState.openDialog?.name !== "elementLinkSelector";
return /* @__PURE__ */ jsx137(FixedSideContainer, { side: "top", children: /* @__PURE__ */ jsxs73("div", { className: "App-menu App-menu_top", children: [
/* @__PURE__ */ jsxs73(Stack_default.Col, { gap: 6, className: clsx53("App-menu_top__left"), children: [
renderCanvasActions(),
shouldRenderSelectedShapeActions && renderSelectedShapeActions()
] }),
!appState.viewModeEnabled && appState.openDialog?.name !== "elementLinkSelector" && /* @__PURE__ */ jsx137(Section, { heading: "shapes", className: "shapes-section", children: (heading) => /* @__PURE__ */ jsxs73("div", { style: { position: "relative" }, children: [
renderWelcomeScreen && /* @__PURE__ */ jsx137(tunnels.WelcomeScreenToolbarHintTunnel.Out, {}),
/* @__PURE__ */ jsx137(Stack_default.Col, { gap: 4, align: "start", children: /* @__PURE__ */ jsxs73(
Stack_default.Row,
{
gap: 1,
className: clsx53("App-toolbar-container", {
"zen-mode": appState.zenModeEnabled
}),
children: [
/* @__PURE__ */ jsxs73(
Island,
{
padding: 1,
className: clsx53("App-toolbar", {
"zen-mode": appState.zenModeEnabled
}),
children: [
/* @__PURE__ */ jsx137(
HintViewer,
{
appState,
isMobile: device.editor.isMobile,
device,
app
}
),
heading,
/* @__PURE__ */ jsxs73(Stack_default.Row, { gap: 1, children: [
/* @__PURE__ */ jsx137(
PenModeButton,
{
zenModeEnabled: appState.zenModeEnabled,
checked: appState.penMode,
onChange: () => onPenModeToggle(null),
title: t("toolBar.penMode"),
penDetected: appState.penDetected
}
),
/* @__PURE__ */ jsx137(
LockButton,
{
checked: appState.activeTool.locked,
onChange: onLockToggle,
title: t("toolBar.lock")
}
),
/* @__PURE__ */ jsx137("div", { className: "App-toolbar__divider" }),
/* @__PURE__ */ jsx137(
HandButton,
{
checked: isHandToolActive(appState),
onChange: () => onHandToolToggle(),
title: t("toolBar.hand"),
isMobile: true
}
),
/* @__PURE__ */ jsx137(
ShapesSwitcher,
{
appState,
activeTool: appState.activeTool,
UIOptions,
app
}
)
] })
]
}
),
isCollaborating && /* @__PURE__ */ jsx137(
Island,
{
style: {
marginLeft: 8,
alignSelf: "center",
height: "fit-content"
},
children: /* @__PURE__ */ jsx137(
LaserPointerButton,
{
title: t("toolBar.laser"),
checked: appState.activeTool.type === TOOL_TYPE.laser,
onChange: () => app.setActiveTool({ type: TOOL_TYPE.laser }),
isMobile: true
}
)
}
)
]
}
) })
] }) }),
/* @__PURE__ */ jsxs73(
"div",
{
className: clsx53(
"layer-ui__wrapper__top-right zen-mode-transition",
{
"transition-right": appState.zenModeEnabled
}
),
children: [
appState.collaborators.size > 0 && /* @__PURE__ */ jsx137(
UserList,
{
collaborators: appState.collaborators,
userToFollow: appState.userToFollow?.socketId || null
}
),
renderTopRightUI?.(device.editor.isMobile, appState),
!appState.viewModeEnabled && appState.openDialog?.name !== "elementLinkSelector" && // hide button when sidebar docked
(!isSidebarDocked || appState.openSidebar?.name !== DEFAULT_SIDEBAR.name) && /* @__PURE__ */ jsx137(tunnels.DefaultSidebarTriggerTunnel.Out, {}),
shouldShowStats && /* @__PURE__ */ jsx137(
Stats,
{
app,
onClose: () => {
actionManager.executeAction(actionToggleStats);
},
renderCustomStats
}
)
]
}
)
] }) });
};
const renderSidebars = () => {
return /* @__PURE__ */ jsx137(
DefaultSidebar,
{
__fallback: true,
onDock: (docked) => {
trackEvent(
"sidebar",
`toggleDock (${docked ? "dock" : "undock"})`,
`(${device.editor.isMobile ? "mobile" : "desktop"})`
);
}
}
);
};
const isSidebarDocked = useAtomValue(isSidebarDockedAtom);
const layerUIJSX = /* @__PURE__ */ jsxs73(Fragment22, { children: [
children,
/* @__PURE__ */ jsx137(DefaultMainMenu, { UIOptions }),
/* @__PURE__ */ jsx137(
DefaultSidebar.Trigger,
{
__fallback: true,
icon: LibraryIcon,
title: capitalizeString(t("toolBar.library")),
onToggle: (open) => {
if (open) {
trackEvent(
"sidebar",
`${DEFAULT_SIDEBAR.name} (open)`,
`button (${device.editor.isMobile ? "mobile" : "desktop"})`
);
}
},
tab: DEFAULT_SIDEBAR.defaultTab,
children: t("toolBar.library")
}
),
/* @__PURE__ */ jsx137(DefaultOverwriteConfirmDialog, {}),
appState.openDialog?.name === "ttd" && /* @__PURE__ */ jsx137(TTDDialog, { __fallback: true }),
appState.isLoading && /* @__PURE__ */ jsx137(LoadingMessage, { delay: 250 }),
appState.errorMessage && /* @__PURE__ */ jsx137(ErrorDialog, { onClose: () => setAppState({ errorMessage: null }), children: appState.errorMessage }),
eyeDropperState && !device.editor.isMobile && /* @__PURE__ */ jsx137(
EyeDropper,
{
colorPickerType: eyeDropperState.colorPickerType,
onCancel: () => {
setEyeDropperState(null);
},
onChange: (colorPickerType, color, selectedElements, { altKey }) => {
if (colorPickerType !== "elementBackground" && colorPickerType !== "elementStroke") {
return;
}
if (selectedElements.length) {
for (const element of selectedElements) {
mutateElement(
element,
{
[altKey && eyeDropperState.swapPreviewOnAlt ? colorPickerType === "elementBackground" ? "strokeColor" : "backgroundColor" : colorPickerType === "elementBackground" ? "backgroundColor" : "strokeColor"]: color
},
false
);
ShapeCache.delete(element);
}
Scene_default.getScene(selectedElements[0])?.triggerUpdate();
} else if (colorPickerType === "elementBackground") {
setAppState({
currentItemBackgroundColor: color
});
} else {
setAppState({ currentItemStrokeColor: color });
}
},
onSelect: (color, event) => {
setEyeDropperState((state) => {
return state?.keepOpenOnAlt && event.altKey ? state : null;
});
eyeDropperState?.onSelect?.(color, event);
}
}
),
appState.openDialog?.name === "help" && /* @__PURE__ */ jsx137(
HelpDialog,
{
onClose: () => {
setAppState({ openDialog: null });
}
}
),
/* @__PURE__ */ jsx137(ActiveConfirmDialog, {}),
appState.openDialog?.name === "elementLinkSelector" && /* @__PURE__ */ jsx137(
ElementLinkDialog_default,
{
sourceElementId: appState.openDialog.sourceElementId,
onClose: () => {
setAppState({
openDialog: null
});
},
elementsMap: app.scene.getNonDeletedElementsMap(),
appState,
generateLinkForSelection
}
),
/* @__PURE__ */ jsx137(tunnels.OverwriteConfirmDialogTunnel.Out, {}),
renderImageExportDialog(),
renderJSONExportDialog(),
appState.pasteDialog.shown && /* @__PURE__ */ jsx137(
PasteChartDialog,
{
setAppState,
appState,
onClose: () => setAppState({
pasteDialog: { shown: false, data: null }
})
}
),
device.editor.isMobile && /* @__PURE__ */ jsx137(
MobileMenu,
{
app,
appState,
elements,
actionManager,
renderJSONExportDialog,
renderImageExportDialog,
setAppState,
onLockToggle,
onHandToolToggle,
onPenModeToggle,
renderTopRightUI,
renderCustomStats,
renderSidebars,
device,
renderWelcomeScreen,
UIOptions
}
),
!device.editor.isMobile && /* @__PURE__ */ jsxs73(Fragment22, { children: [
/* @__PURE__ */ jsxs73(
"div",
{
className: "layer-ui__wrapper",
style: appState.openSidebar && isSidebarDocked && device.editor.canFitSidebar ? { width: `calc(100% - var(--right-sidebar-width))` } : {},
children: [
renderWelcomeScreen && /* @__PURE__ */ jsx137(tunnels.WelcomeScreenCenterTunnel.Out, {}),
renderFixedSideContainer(),
/* @__PURE__ */ jsx137(
Footer_default,
{
appState,
actionManager,
showExitZenModeBtn,
renderWelcomeScreen
}
),
appState.scrolledOutside && /* @__PURE__ */ jsx137(
"button",
{
type: "button",
className: "scroll-back-to-content",
onClick: () => {
setAppState((appState2) => ({
...calculateScrollCenter(elements, appState2)
}));
},
children: t("buttons.scrollBackToContent")
}
)
]
}
),
renderSidebars()
] })
] });
return /* @__PURE__ */ jsx137(UIAppStateContext.Provider, { value: appState, children: /* @__PURE__ */ jsx137(TunnelsJotaiProvider, { children: /* @__PURE__ */ jsx137(TunnelsContext.Provider, { value: tunnels, children: layerUIJSX }) }) });
};
var stripIrrelevantAppStateProps = (appState) => {
const {
suggestedBindings,
startBoundElement,
cursorButton,
scrollX,
scrollY,
...ret
} = appState;
return ret;
};
var areEqual2 = (prevProps, nextProps) => {
if (prevProps.children !== nextProps.children) {
return false;
}
const { canvas: _pC, appState: prevAppState, ...prev } = prevProps;
const { canvas: _nC, appState: nextAppState, ...next } = nextProps;
return isShallowEqual(
// asserting AppState because we're being passed the whole AppState
// but resolve to only the UI-relevant props
stripIrrelevantAppStateProps(prevAppState),
stripIrrelevantAppStateProps(nextAppState),
{
selectedElementIds: isShallowEqual,
selectedGroupIds: isShallowEqual
}
) && isShallowEqual(prev, next);
};
var LayerUI_default = React40.memo(LayerUI, areEqual2);
// components/Toast.tsx
import { useCallback as useCallback14, useEffect as useEffect40, useRef as useRef35 } from "react";
import { jsx as jsx138, jsxs as jsxs74 } from "react/jsx-runtime";
var DEFAULT_TOAST_TIMEOUT = 5e3;
var Toast = ({
message,
onClose,
closable = false,
// To prevent autoclose, pass duration as Infinity
duration = DEFAULT_TOAST_TIMEOUT,
style
}) => {
const timerRef = useRef35(0);
const shouldAutoClose = duration !== Infinity;
const scheduleTimeout = useCallback14(() => {
if (!shouldAutoClose) {
return;
}
timerRef.current = window.setTimeout(() => onClose(), duration);
}, [onClose, duration, shouldAutoClose]);
useEffect40(() => {
if (!shouldAutoClose) {
return;
}
scheduleTimeout();
return () => clearTimeout(timerRef.current);
}, [scheduleTimeout, message, duration, shouldAutoClose]);
const onMouseEnter = shouldAutoClose ? () => clearTimeout(timerRef?.current) : void 0;
const onMouseLeave = shouldAutoClose ? scheduleTimeout : void 0;
return /* @__PURE__ */ jsxs74(
"div",
{
className: "Toast",
onMouseEnter,
onMouseLeave,
style,
children: [
/* @__PURE__ */ jsx138("p", { className: "Toast__message", children: message }),
closable && /* @__PURE__ */ jsx138(
ToolButton,
{
icon: CloseIcon,
"aria-label": "close",
type: "icon",
onClick: onClose,
className: "close"
}
)
]
}
);
};
// actions/actionToggleViewMode.tsx
var actionToggleViewMode = register({
name: "viewMode",
label: "labels.viewMode",
paletteName: "Toggle view mode",
icon: eyeIcon,
viewMode: true,
trackEvent: {
category: "canvas",
predicate: (appState) => !appState.viewModeEnabled
},
perform(elements, appState) {
return {
appState: {
...appState,
viewModeEnabled: !this.checked(appState)
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
checked: (appState) => appState.viewModeEnabled,
predicate: (elements, appState, appProps) => {
return typeof appProps.viewModeEnabled === "undefined";
},
keyTest: (event) => !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.R
});
// components/App.tsx
import throttle3 from "lodash.throttle";
// actions/actionFrame.ts
var isSingleFrameSelected = (appState, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
return selectedElements.length === 1 && isFrameLikeElement(selectedElements[0]);
};
var actionSelectAllElementsInFrame = register({
name: "selectAllElementsInFrame",
label: "labels.selectAllElementsInFrame",
trackEvent: { category: "canvas" },
perform: (elements, appState, _, app) => {
const selectedElement = app.scene.getSelectedElements(appState).at(0) || null;
if (isFrameLikeElement(selectedElement)) {
const elementsInFrame = getFrameChildren(
getNonDeletedElements(elements),
selectedElement.id
).filter((element) => !(element.type === "text" && element.containerId));
return {
elements,
appState: {
...appState,
selectedElementIds: elementsInFrame.reduce((acc, element) => {
acc[element.id] = true;
return acc;
}, {})
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
return {
elements,
appState,
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
predicate: (elements, appState, _, app) => isSingleFrameSelected(appState, app)
});
var actionRemoveAllElementsFromFrame = register({
name: "removeAllElementsFromFrame",
label: "labels.removeAllElementsFromFrame",
trackEvent: { category: "history" },
perform: (elements, appState, _, app) => {
const selectedElement = app.scene.getSelectedElements(appState).at(0) || null;
if (isFrameLikeElement(selectedElement)) {
return {
elements: removeAllElementsFromFrame(elements, selectedElement),
appState: {
...appState,
selectedElementIds: {
[selectedElement.id]: true
}
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
return {
elements,
appState,
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
predicate: (elements, appState, _, app) => isSingleFrameSelected(appState, app)
});
var actionupdateFrameRendering = register({
name: "updateFrameRendering",
label: "labels.updateFrameRendering",
viewMode: true,
trackEvent: { category: "canvas" },
perform: (elements, appState) => {
return {
elements,
appState: {
...appState,
frameRendering: {
...appState.frameRendering,
enabled: !appState.frameRendering.enabled
}
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
checked: (appState) => appState.frameRendering.enabled
});
var actionSetFrameAsActiveTool = register({
name: "setFrameAsActiveTool",
label: "toolBar.frame",
trackEvent: { category: "toolbar" },
icon: frameToolIcon,
viewMode: false,
perform: (elements, appState, _, app) => {
const nextActiveTool = updateActiveTool(appState, {
type: "frame"
});
setCursorForShape(app.interactiveCanvas, {
...appState,
activeTool: nextActiveTool
});
return {
elements,
appState: {
...appState,
activeTool: updateActiveTool(appState, {
type: "frame"
})
},
captureUpdate: CaptureUpdateAction.EVENTUALLY
};
},
keyTest: (event) => !event[KEYS.CTRL_OR_CMD] && !event.shiftKey && !event.altKey && event.key.toLocaleLowerCase() === KEYS.F
});
var actionWrapSelectionInFrame = register({
name: "wrapSelectionInFrame",
label: "labels.wrapSelectionInFrame",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length > 0 && !selectedElements.some((element) => isFrameLikeElement(element));
},
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState);
const [x1, y1, x2, y2] = getCommonBounds(
selectedElements,
app.scene.getNonDeletedElementsMap()
);
const PADDING = 16;
const frame = newFrameElement({
x: x1 - PADDING,
y: y1 - PADDING,
width: x2 - x1 + PADDING * 2,
height: y2 - y1 + PADDING * 2
});
if (appState.editingGroupId) {
const elementsInGroup = getElementsInGroup(
selectedElements,
appState.editingGroupId
);
for (const elementInGroup of elementsInGroup) {
const index = elementInGroup.groupIds.indexOf(appState.editingGroupId);
mutateElement(
elementInGroup,
{
groupIds: elementInGroup.groupIds.slice(0, index)
},
false
);
}
}
const nextElements = addElementsToFrame(
[...app.scene.getElementsIncludingDeleted(), frame],
selectedElements,
frame,
appState
);
return {
elements: nextElements,
appState: {
selectedElementIds: { [frame.id]: true }
},
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
});
// components/BraveMeasureTextError.tsx
import { jsx as jsx139, jsxs as jsxs75 } from "react/jsx-runtime";
var BraveMeasureTextError = () => {
return /* @__PURE__ */ jsxs75("div", { "data-testid": "brave-measure-text-error", children: [
/* @__PURE__ */ jsx139("p", { children: /* @__PURE__ */ jsx139(
Trans_default,
{
i18nKey: "errors.brave_measure_text_error.line1",
bold: (el) => /* @__PURE__ */ jsx139("span", { style: { fontWeight: 600 }, children: el })
}
) }),
/* @__PURE__ */ jsx139("p", { children: /* @__PURE__ */ jsx139(
Trans_default,
{
i18nKey: "errors.brave_measure_text_error.line2",
bold: (el) => /* @__PURE__ */ jsx139("span", { style: { fontWeight: 600 }, children: el })
}
) }),
/* @__PURE__ */ jsx139("p", { children: /* @__PURE__ */ jsx139(
Trans_default,
{
i18nKey: "errors.brave_measure_text_error.line3",
link: (el) => /* @__PURE__ */ jsx139("a", { href: "http://docs.excalidraw.com/docs/@excalidraw/excalidraw/faq#turning-off-aggresive-block-fingerprinting-in-brave-browser", children: el })
}
) }),
/* @__PURE__ */ jsx139("p", { children: /* @__PURE__ */ jsx139(
Trans_default,
{
i18nKey: "errors.brave_measure_text_error.line4",
issueLink: (el) => /* @__PURE__ */ jsx139("a", { href: "https://github.com/excalidraw/excalidraw/issues/new", children: el }),
discordLink: (el) => /* @__PURE__ */ jsxs75("a", { href: "https://discord.gg/UexuTaE", children: [
el,
"."
] })
}
) })
] });
};
var BraveMeasureTextError_default = BraveMeasureTextError;
// data/transform.ts
var DEFAULT_LINEAR_ELEMENT_PROPS = {
width: 100,
height: 0
};
var DEFAULT_DIMENSION = 100;
var bindTextToContainer = (container, textProps, elementsMap) => {
const textElement = newTextElement({
x: 0,
y: 0,
textAlign: TEXT_ALIGN.CENTER,
verticalAlign: VERTICAL_ALIGN.MIDDLE,
...textProps,
containerId: container.id,
strokeColor: textProps.strokeColor || container.strokeColor
});
Object.assign(container, {
boundElements: (container.boundElements || []).concat({
type: "text",
id: textElement.id
})
});
redrawTextBoundingBox(textElement, container, elementsMap);
return [container, textElement];
};
var bindLinearElementToElement = (linearElement, start, end, elementStore, elementsMap) => {
let startBoundElement;
let endBoundElement;
Object.assign(linearElement, {
startBinding: linearElement?.startBinding || null,
endBinding: linearElement.endBinding || null
});
if (start) {
const width = start?.width ?? DEFAULT_DIMENSION;
const height = start?.height ?? DEFAULT_DIMENSION;
let existingElement;
if (start.id) {
existingElement = elementStore.getElement(start.id);
if (!existingElement) {
console.error(`No element for start binding with id ${start.id} found`);
}
}
const startX = start.x || linearElement.x - width;
const startY = start.y || linearElement.y - height / 2;
const startType = existingElement ? existingElement.type : start.type;
if (startType) {
if (startType === "text") {
let text = "";
if (existingElement && existingElement.type === "text") {
text = existingElement.text;
} else if (start.type === "text") {
text = start.text;
}
if (!text) {
console.error(
`No text found for start binding text element for ${linearElement.id}`
);
}
startBoundElement = newTextElement({
x: startX,
y: startY,
type: "text",
...existingElement,
...start,
text
});
Object.assign(startBoundElement, {
x: start.x || linearElement.x - startBoundElement.width,
y: start.y || linearElement.y - startBoundElement.height / 2
});
} else {
switch (startType) {
case "rectangle":
case "ellipse":
case "diamond": {
startBoundElement = newElement({
x: startX,
y: startY,
width,
height,
...existingElement,
...start,
type: startType
});
break;
}
default: {
assertNever(
linearElement,
`Unhandled element start type "${start.type}"`,
true
);
}
}
}
bindLinearElement(
linearElement,
startBoundElement,
"start",
elementsMap
);
}
}
if (end) {
const height = end?.height ?? DEFAULT_DIMENSION;
const width = end?.width ?? DEFAULT_DIMENSION;
let existingElement;
if (end.id) {
existingElement = elementStore.getElement(end.id);
if (!existingElement) {
console.error(`No element for end binding with id ${end.id} found`);
}
}
const endX = end.x || linearElement.x + linearElement.width;
const endY = end.y || linearElement.y - height / 2;
const endType = existingElement ? existingElement.type : end.type;
if (endType) {
if (endType === "text") {
let text = "";
if (existingElement && existingElement.type === "text") {
text = existingElement.text;
} else if (end.type === "text") {
text = end.text;
}
if (!text) {
console.error(
`No text found for end binding text element for ${linearElement.id}`
);
}
endBoundElement = newTextElement({
x: endX,
y: endY,
type: "text",
...existingElement,
...end,
text
});
Object.assign(endBoundElement, {
y: end.y || linearElement.y - endBoundElement.height / 2
});
} else {
switch (endType) {
case "rectangle":
case "ellipse":
case "diamond": {
endBoundElement = newElement({
x: endX,
y: endY,
width,
height,
...existingElement,
...end,
type: endType
});
break;
}
default: {
assertNever(
linearElement,
`Unhandled element end type "${endType}"`,
true
);
}
}
}
bindLinearElement(
linearElement,
endBoundElement,
"end",
elementsMap
);
}
}
if (linearElement.points.length < 2) {
return {
linearElement,
startBoundElement,
endBoundElement
};
}
const endPointIndex = linearElement.points.length - 1;
const delta = 0.5;
const newPoints = cloneJSON(linearElement.points);
if (linearElement.points[endPointIndex][0] > linearElement.points[endPointIndex - 1][0]) {
newPoints[0][0] = delta;
newPoints[endPointIndex][0] -= delta;
}
if (linearElement.points[endPointIndex][0] < linearElement.points[endPointIndex - 1][0]) {
newPoints[0][0] = -delta;
newPoints[endPointIndex][0] += delta;
}
if (linearElement.points[endPointIndex][1] > linearElement.points[endPointIndex - 1][1]) {
newPoints[0][1] = delta;
newPoints[endPointIndex][1] -= delta;
}
if (linearElement.points[endPointIndex][1] < linearElement.points[endPointIndex - 1][1]) {
newPoints[0][1] = -delta;
newPoints[endPointIndex][1] += delta;
}
Object.assign(linearElement, { points: newPoints });
return {
linearElement,
startBoundElement,
endBoundElement
};
};
var ElementStore = class {
constructor() {
__publicField(this, "excalidrawElements", /* @__PURE__ */ new Map());
__publicField(this, "add", (ele) => {
if (!ele) {
return;
}
this.excalidrawElements.set(ele.id, ele);
});
__publicField(this, "getElements", () => {
return syncInvalidIndices(Array.from(this.excalidrawElements.values()));
});
__publicField(this, "getElementsMap", () => {
return toBrandedType(
arrayToMap(this.getElements())
);
});
__publicField(this, "getElement", (id) => {
return this.excalidrawElements.get(id);
});
}
};
var convertToExcalidrawElements = (elementsSkeleton, opts) => {
if (!elementsSkeleton) {
return [];
}
const elements = cloneJSON(elementsSkeleton);
const elementStore = new ElementStore();
const elementsWithIds = /* @__PURE__ */ new Map();
const oldToNewElementIdMap = /* @__PURE__ */ new Map();
for (const element of elements) {
let excalidrawElement;
const originalId = element.id;
if (opts?.regenerateIds !== false) {
Object.assign(element, { id: randomId() });
}
switch (element.type) {
case "rectangle":
case "ellipse":
case "diamond": {
const width = element?.label?.text && element.width === void 0 ? 0 : element?.width || DEFAULT_DIMENSION;
const height = element?.label?.text && element.height === void 0 ? 0 : element?.height || DEFAULT_DIMENSION;
excalidrawElement = newElement({
...element,
width,
height
});
break;
}
case "line": {
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
excalidrawElement = newLinearElement({
width,
height,
points: [pointFrom(0, 0), pointFrom(width, height)],
...element
});
break;
}
case "arrow": {
const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width;
const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height;
excalidrawElement = newArrowElement({
width,
height,
endArrowhead: "arrow",
points: [pointFrom(0, 0), pointFrom(width, height)],
...element,
type: "arrow"
});
Object.assign(
excalidrawElement,
getSizeFromPoints(excalidrawElement.points)
);
break;
}
case "text": {
const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY;
const fontSize = element?.fontSize || DEFAULT_FONT_SIZE;
const lineHeight = element?.lineHeight || getLineHeight(fontFamily);
const text = element.text ?? "";
const normalizedText = normalizeText(text);
const metrics = measureText(
normalizedText,
getFontString({ fontFamily, fontSize }),
lineHeight
);
excalidrawElement = newTextElement({
width: metrics.width,
height: metrics.height,
fontFamily,
fontSize,
...element
});
break;
}
case "image": {
excalidrawElement = newImageElement({
width: element?.width || DEFAULT_DIMENSION,
height: element?.height || DEFAULT_DIMENSION,
...element
});
break;
}
case "frame": {
excalidrawElement = newFrameElement({
x: 0,
y: 0,
...element
});
break;
}
case "magicframe": {
excalidrawElement = newMagicFrameElement({
x: 0,
y: 0,
...element
});
break;
}
case "freedraw":
case "iframe":
case "embeddable": {
excalidrawElement = element;
break;
}
default: {
excalidrawElement = element;
assertNever(
element,
`Unhandled element type "${element.type}"`,
true
);
}
}
const existingElement = elementStore.getElement(excalidrawElement.id);
if (existingElement) {
console.error(`Duplicate id found for ${excalidrawElement.id}`);
} else {
elementStore.add(excalidrawElement);
elementsWithIds.set(excalidrawElement.id, element);
if (originalId) {
oldToNewElementIdMap.set(originalId, excalidrawElement.id);
}
}
}
const elementsMap = elementStore.getElementsMap();
for (const [id, element] of elementsWithIds) {
const excalidrawElement = elementStore.getElement(id);
switch (element.type) {
case "rectangle":
case "ellipse":
case "diamond":
case "arrow": {
if (element.label?.text) {
let [container, text] = bindTextToContainer(
excalidrawElement,
element?.label,
elementsMap
);
elementStore.add(container);
elementStore.add(text);
if (isArrowElement(container)) {
const originalStart = element.type === "arrow" ? element?.start : void 0;
const originalEnd = element.type === "arrow" ? element?.end : void 0;
if (originalStart && originalStart.id) {
const newStartId = oldToNewElementIdMap.get(originalStart.id);
if (newStartId) {
Object.assign(originalStart, { id: newStartId });
}
}
if (originalEnd && originalEnd.id) {
const newEndId = oldToNewElementIdMap.get(originalEnd.id);
if (newEndId) {
Object.assign(originalEnd, { id: newEndId });
}
}
const { linearElement, startBoundElement, endBoundElement } = bindLinearElementToElement(
container,
originalStart,
originalEnd,
elementStore,
elementsMap
);
container = linearElement;
elementStore.add(linearElement);
elementStore.add(startBoundElement);
elementStore.add(endBoundElement);
}
} else {
switch (element.type) {
case "arrow": {
const { start, end } = element;
if (start && start.id) {
const newStartId = oldToNewElementIdMap.get(start.id);
Object.assign(start, { id: newStartId });
}
if (end && end.id) {
const newEndId = oldToNewElementIdMap.get(end.id);
Object.assign(end, { id: newEndId });
}
const { linearElement, startBoundElement, endBoundElement } = bindLinearElementToElement(
excalidrawElement,
start,
end,
elementStore,
elementsMap
);
elementStore.add(linearElement);
elementStore.add(startBoundElement);
elementStore.add(endBoundElement);
break;
}
}
}
break;
}
}
}
for (const [id, element] of elementsWithIds) {
if (element.type !== "frame" && element.type !== "magicframe") {
continue;
}
const frame = elementStore.getElement(id);
if (!frame) {
throw new Error(`Excalidraw element with id ${id} doesn't exist`);
}
const childrenElements = [];
element.children.forEach((id2) => {
const newElementId = oldToNewElementIdMap.get(id2);
if (!newElementId) {
throw new Error(`Element with ${id2} wasn't mapped correctly`);
}
const elementInFrame = elementStore.getElement(newElementId);
if (!elementInFrame) {
throw new Error(`Frame element with id ${newElementId} doesn't exist`);
}
Object.assign(elementInFrame, { frameId: frame.id });
elementInFrame?.boundElements?.forEach((boundElement) => {
const ele = elementStore.getElement(boundElement.id);
if (!ele) {
throw new Error(
`Bound element with id ${boundElement.id} doesn't exist`
);
}
Object.assign(ele, { frameId: frame.id });
childrenElements.push(ele);
});
childrenElements.push(elementInFrame);
});
let [minX, minY, maxX, maxY] = getCommonBounds(childrenElements);
const PADDING = 10;
minX = minX - PADDING;
minY = minY - PADDING;
maxX = maxX + PADDING;
maxY = maxY + PADDING;
const frameX = frame?.x || minX;
const frameY = frame?.y || minY;
const frameWidth = frame?.width || maxX - minX;
const frameHeight = frame?.height || maxY - minY;
Object.assign(frame, {
x: frameX,
y: frameY,
width: frameWidth,
height: frameHeight
});
if (isDevEnv() && element.children.length && (frame?.x || frame?.y || frame?.width || frame?.height)) {
console.info(
"User provided frame attributes are being considered, if you find this inaccurate, please remove any of the attributes - x, y, width and height so frame coordinates and dimensions are calculated automatically"
);
}
}
return elementStore.getElements();
};
// components/canvases/InteractiveCanvas.tsx
import React41, { useEffect as useEffect41, useRef as useRef36 } from "react";
// reactUtils.ts
import { unstable_batchedUpdates as unstable_batchedUpdates2 } from "react-dom";
import { version as ReactVersion } from "react";
var withBatchedUpdates = (func) => (event) => {
unstable_batchedUpdates2(func, event);
};
var withBatchedUpdatesThrottled = (func) => {
return throttleRAF((event) => {
unstable_batchedUpdates2(func, event);
});
};
var isRenderThrottlingEnabled = (() => {
let IS_REACT_18_AND_UP;
try {
const version = ReactVersion.split(".");
IS_REACT_18_AND_UP = Number(version[0]) > 17;
} catch {
IS_REACT_18_AND_UP = false;
}
let hasWarned = false;
return () => {
if (window.EXCALIDRAW_THROTTLE_RENDER === true) {
if (!IS_REACT_18_AND_UP) {
if (!hasWarned) {
hasWarned = true;
console.warn(
"Excalidraw: render throttling is disabled on React versions < 18."
);
}
return false;
}
return true;
}
return false;
};
})();
// renderer/renderSnaps.ts
var SNAP_COLOR_LIGHT = "#ff6b6b";
var SNAP_COLOR_DARK = "#ff0000";
var SNAP_WIDTH = 1;
var SNAP_CROSS_SIZE = 2;
var renderSnaps = (context, appState) => {
if (!appState.snapLines.length) {
return;
}
const snapColor = appState.theme === THEME.LIGHT || appState.zenModeEnabled ? SNAP_COLOR_LIGHT : SNAP_COLOR_DARK;
const snapWidth = (appState.zenModeEnabled ? SNAP_WIDTH * 1.5 : SNAP_WIDTH) / appState.zoom.value;
context.save();
context.translate(appState.scrollX, appState.scrollY);
for (const snapLine of appState.snapLines) {
if (snapLine.type === "pointer") {
context.lineWidth = snapWidth;
context.strokeStyle = snapColor;
drawPointerSnapLine(snapLine, context, appState);
} else if (snapLine.type === "gap") {
context.lineWidth = snapWidth;
context.strokeStyle = snapColor;
drawGapLine(
snapLine.points[0],
snapLine.points[1],
snapLine.direction,
appState,
context
);
} else if (snapLine.type === "points") {
context.lineWidth = snapWidth;
context.strokeStyle = snapColor;
drawPointsSnapLine(snapLine, context, appState);
}
}
context.restore();
};
var drawPointsSnapLine = (pointSnapLine, context, appState) => {
if (!appState.zenModeEnabled) {
const firstPoint = pointSnapLine.points[0];
const lastPoint = pointSnapLine.points[pointSnapLine.points.length - 1];
drawLine(firstPoint, lastPoint, context);
}
for (const point of pointSnapLine.points) {
drawCross(point, appState, context);
}
};
var drawPointerSnapLine = (pointerSnapLine, context, appState) => {
drawCross(pointerSnapLine.points[0], appState, context);
if (!appState.zenModeEnabled) {
drawLine(pointerSnapLine.points[0], pointerSnapLine.points[1], context);
}
};
var drawCross = ([x, y], appState, context) => {
context.save();
const size = (appState.zenModeEnabled ? SNAP_CROSS_SIZE * 1.5 : SNAP_CROSS_SIZE) / appState.zoom.value;
context.beginPath();
context.moveTo(x - size, y - size);
context.lineTo(x + size, y + size);
context.moveTo(x + size, y - size);
context.lineTo(x - size, y + size);
context.stroke();
context.restore();
};
var drawLine = (from, to, context) => {
context.beginPath();
context.lineTo(from[0], from[1]);
context.lineTo(to[0], to[1]);
context.stroke();
};
var drawGapLine = (from, to, direction, appState, context) => {
const FULL = 8 / appState.zoom.value;
const HALF = FULL / 2;
const QUARTER = FULL / 4;
if (direction === "horizontal") {
const halfPoint = [(from[0] + to[0]) / 2, from[1]];
if (!appState.zenModeEnabled) {
drawLine(
pointFrom(from[0], from[1] - FULL),
pointFrom(from[0], from[1] + FULL),
context
);
}
drawLine(
pointFrom(halfPoint[0] - QUARTER, halfPoint[1] - HALF),
pointFrom(halfPoint[0] - QUARTER, halfPoint[1] + HALF),
context
);
drawLine(
pointFrom(halfPoint[0] + QUARTER, halfPoint[1] - HALF),
pointFrom(halfPoint[0] + QUARTER, halfPoint[1] + HALF),
context
);
if (!appState.zenModeEnabled) {
drawLine(
pointFrom(to[0], to[1] - FULL),
pointFrom(to[0], to[1] + FULL),
context
);
drawLine(from, to, context);
}
} else {
const halfPoint = [from[0], (from[1] + to[1]) / 2];
if (!appState.zenModeEnabled) {
drawLine(
pointFrom(from[0] - FULL, from[1]),
pointFrom(from[0] + FULL, from[1]),
context
);
}
drawLine(
pointFrom(halfPoint[0] - HALF, halfPoint[1] - QUARTER),
pointFrom(halfPoint[0] + HALF, halfPoint[1] - QUARTER),
context
);
drawLine(
pointFrom(halfPoint[0] - HALF, halfPoint[1] + QUARTER),
pointFrom(halfPoint[0] + HALF, halfPoint[1] + QUARTER),
context
);
if (!appState.zenModeEnabled) {
drawLine(
pointFrom(to[0] - FULL, to[1]),
pointFrom(to[0] + FULL, to[1]),
context
);
drawLine(from, to, context);
}
}
};
// renderer/interactiveScene.ts
import oc2 from "open-color";
var renderElbowArrowMidPointHighlight = (context, appState) => {
invariant(appState.selectedLinearElement, "selectedLinearElement is null");
const { segmentMidPointHoveredCoords } = appState.selectedLinearElement;
invariant(segmentMidPointHoveredCoords, "midPointCoords is null");
context.save();
context.translate(appState.scrollX, appState.scrollY);
highlightPoint(segmentMidPointHoveredCoords, context, appState);
context.restore();
};
var renderLinearElementPointHighlight = (context, appState, elementsMap) => {
const { elementId, hoverPointIndex } = appState.selectedLinearElement;
if (appState.editingLinearElement?.selectedPointsIndices?.includes(
hoverPointIndex
)) {
return;
}
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) {
return;
}
const point = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
hoverPointIndex,
elementsMap
);
context.save();
context.translate(appState.scrollX, appState.scrollY);
highlightPoint(point, context, appState);
context.restore();
};
var highlightPoint = (point, context, appState) => {
context.fillStyle = "rgba(105, 101, 219, 0.4)";
fillCircle(
context,
point[0],
point[1],
LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value,
false
);
};
var strokeRectWithRotation = (context, x, y, width, height, cx, cy, angle, fill = false, radius = 0) => {
context.save();
context.translate(cx, cy);
context.rotate(angle);
if (fill) {
context.fillRect(x - cx, y - cy, width, height);
}
if (radius && context.roundRect) {
context.beginPath();
context.roundRect(x - cx, y - cy, width, height, radius);
context.stroke();
context.closePath();
} else {
context.strokeRect(x - cx, y - cy, width, height);
}
context.restore();
};
var strokeDiamondWithRotation = (context, width, height, cx, cy, angle) => {
context.save();
context.translate(cx, cy);
context.rotate(angle);
context.beginPath();
context.moveTo(0, height / 2);
context.lineTo(width / 2, 0);
context.lineTo(0, -height / 2);
context.lineTo(-width / 2, 0);
context.closePath();
context.stroke();
context.restore();
};
var renderSingleLinearPoint = (context, appState, point, radius, isSelected, isPhantomPoint = false) => {
context.strokeStyle = "#5e5ad8";
context.setLineDash([]);
context.fillStyle = "rgba(255, 255, 255, 0.9)";
if (isSelected) {
context.fillStyle = "rgba(134, 131, 226, 0.9)";
} else if (isPhantomPoint) {
context.fillStyle = "rgba(177, 151, 252, 0.7)";
}
fillCircle(
context,
point[0],
point[1],
radius / appState.zoom.value,
!isPhantomPoint
);
};
var strokeEllipseWithRotation = (context, width, height, cx, cy, angle) => {
context.beginPath();
context.ellipse(cx, cy, width / 2, height / 2, angle, 0, Math.PI * 2);
context.stroke();
};
var renderBindingHighlightForBindableElement = (context, element, elementsMap, zoom) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const width = x2 - x1;
const height = y2 - y1;
context.strokeStyle = "rgba(0,0,0,.05)";
const zoomValue = zoom.value < 1 ? zoom.value : 1;
context.lineWidth = BINDING_HIGHLIGHT_THICKNESS / zoomValue;
const padding = context.lineWidth / 2 + BINDING_HIGHLIGHT_OFFSET;
const radius = getCornerRadius(
Math.min(element.width, element.height),
element
);
switch (element.type) {
case "rectangle":
case "text":
case "image":
case "iframe":
case "embeddable":
case "frame":
case "magicframe":
strokeRectWithRotation(
context,
x1 - padding,
y1 - padding,
width + padding * 2,
height + padding * 2,
x1 + width / 2,
y1 + height / 2,
element.angle,
void 0,
radius
);
break;
case "diamond":
const side = Math.hypot(width, height);
const wPadding = padding * side / height;
const hPadding = padding * side / width;
strokeDiamondWithRotation(
context,
width + wPadding * 2,
height + hPadding * 2,
x1 + width / 2,
y1 + height / 2,
element.angle
);
break;
case "ellipse":
strokeEllipseWithRotation(
context,
width + padding * 2,
height + padding * 2,
x1 + width / 2,
y1 + height / 2,
element.angle
);
break;
}
};
var renderBindingHighlightForSuggestedPointBinding = (context, suggestedBinding, elementsMap, zoom) => {
const [element, startOrEnd, bindableElement] = suggestedBinding;
const threshold = maxBindingGap(
bindableElement,
bindableElement.width,
bindableElement.height,
zoom
);
context.strokeStyle = "rgba(0,0,0,0)";
context.fillStyle = "rgba(0,0,0,.05)";
const pointIndices = startOrEnd === "both" ? [0, -1] : startOrEnd === "start" ? [0] : [-1];
pointIndices.forEach((index) => {
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element,
index,
elementsMap
);
fillCircle(context, x, y, threshold);
});
};
var renderSelectionBorder = (context, appState, elementProperties) => {
const {
angle,
x1,
y1,
x2,
y2,
selectionColors,
cx,
cy,
dashed,
activeEmbeddable
} = elementProperties;
const elementWidth = x2 - x1;
const elementHeight = y2 - y1;
const padding = elementProperties.padding ?? DEFAULT_TRANSFORM_HANDLE_SPACING * 2;
const linePadding = padding / appState.zoom.value;
const lineWidth = 8 / appState.zoom.value;
const spaceWidth = 4 / appState.zoom.value;
context.save();
context.translate(appState.scrollX, appState.scrollY);
context.lineWidth = (activeEmbeddable ? 4 : 1) / appState.zoom.value;
const count = selectionColors.length;
for (let index = 0; index < count; ++index) {
context.strokeStyle = selectionColors[index];
if (dashed) {
context.setLineDash([
lineWidth,
spaceWidth + (lineWidth + spaceWidth) * (count - 1)
]);
}
context.lineDashOffset = (lineWidth + spaceWidth) * index;
strokeRectWithRotation(
context,
x1 - linePadding,
y1 - linePadding,
elementWidth + linePadding * 2,
elementHeight + linePadding * 2,
cx,
cy,
angle
);
}
context.restore();
};
var renderBindingHighlight = (context, appState, suggestedBinding, elementsMap) => {
const renderHighlight = Array.isArray(suggestedBinding) ? renderBindingHighlightForSuggestedPointBinding : renderBindingHighlightForBindableElement;
context.save();
context.translate(appState.scrollX, appState.scrollY);
renderHighlight(context, suggestedBinding, elementsMap, appState.zoom);
context.restore();
};
var renderFrameHighlight = (context, appState, frame, elementsMap) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
const width = x2 - x1;
const height = y2 - y1;
context.strokeStyle = "rgb(0,118,255)";
context.lineWidth = FRAME_STYLE.strokeWidth / appState.zoom.value;
context.save();
context.translate(appState.scrollX, appState.scrollY);
strokeRectWithRotation(
context,
x1,
y1,
width,
height,
x1 + width / 2,
y1 + height / 2,
frame.angle,
false,
FRAME_STYLE.radius / appState.zoom.value
);
context.restore();
};
var renderElementsBoxHighlight = (context, appState, elements) => {
const individualElements = elements.filter(
(element) => element.groupIds.length === 0
);
const elementsInGroups = elements.filter(
(element) => element.groupIds.length > 0
);
const getSelectionFromElements = (elements2) => {
const [x1, y1, x2, y2] = getCommonBounds(elements2);
return {
angle: 0,
x1,
x2,
y1,
y2,
selectionColors: ["rgb(0,118,255)"],
dashed: false,
cx: x1 + (x2 - x1) / 2,
cy: y1 + (y2 - y1) / 2,
activeEmbeddable: false
};
};
const getSelectionForGroupId = (groupId) => {
const groupElements = getElementsInGroup(elements, groupId);
return getSelectionFromElements(groupElements);
};
Object.entries(selectGroupsFromGivenElements(elementsInGroups, appState)).filter(([id, isSelected]) => isSelected).map(([id, isSelected]) => id).map((groupId) => getSelectionForGroupId(groupId)).concat(
individualElements.map((element) => getSelectionFromElements([element]))
).forEach(
(selection) => renderSelectionBorder(context, appState, selection)
);
};
var renderLinearPointHandles = (context, appState, element, elementsMap) => {
if (!appState.selectedLinearElement) {
return;
}
context.save();
context.translate(appState.scrollX, appState.scrollY);
context.lineWidth = 1 / appState.zoom.value;
const points = LinearElementEditor.getPointsGlobalCoordinates(
element,
elementsMap
);
const { POINT_HANDLE_SIZE } = LinearElementEditor;
const radius = appState.editingLinearElement ? POINT_HANDLE_SIZE : POINT_HANDLE_SIZE / 2;
points.forEach((point, idx) => {
if (isElbowArrow(element) && idx !== 0 && idx !== points.length - 1) {
return;
}
const isSelected = !!appState.editingLinearElement?.selectedPointsIndices?.includes(idx);
renderSingleLinearPoint(context, appState, point, radius, isSelected);
});
if (isElbowArrow(element)) {
const fixedSegments = element.fixedSegments?.map((segment) => segment.index) || [];
points.slice(0, -1).forEach((p, idx) => {
if (!LinearElementEditor.isSegmentTooShort(
element,
points[idx + 1],
points[idx],
idx,
appState.zoom
)) {
renderSingleLinearPoint(
context,
appState,
pointFrom(
(p[0] + points[idx + 1][0]) / 2,
(p[1] + points[idx + 1][1]) / 2
),
POINT_HANDLE_SIZE / 2,
false,
!fixedSegments.includes(idx + 1)
);
}
});
} else {
const midPoints = LinearElementEditor.getEditorMidPoints(
element,
elementsMap,
appState
).filter(
(midPoint, idx, midPoints2) => midPoint !== null && !(isElbowArrow(element) && (idx === 0 || idx === midPoints2.length - 1))
);
midPoints.forEach((segmentMidPoint) => {
if (appState.editingLinearElement || points.length === 2) {
renderSingleLinearPoint(
context,
appState,
segmentMidPoint,
POINT_HANDLE_SIZE / 2,
false,
true
);
}
});
}
context.restore();
};
var renderTransformHandles = (context, renderConfig, appState, transformHandles, angle) => {
Object.keys(transformHandles).forEach((key) => {
const transformHandle = transformHandles[key];
if (transformHandle !== void 0) {
const [x, y, width, height] = transformHandle;
context.save();
context.lineWidth = 1 / appState.zoom.value;
if (renderConfig.selectionColor) {
context.strokeStyle = renderConfig.selectionColor;
}
if (key === "rotation") {
fillCircle(context, x + width / 2, y + height / 2, width / 2);
} else if (context.roundRect) {
context.beginPath();
context.roundRect(x, y, width, height, 2 / appState.zoom.value);
context.fill();
context.stroke();
} else {
strokeRectWithRotation(
context,
x,
y,
width,
height,
x + width / 2,
y + height / 2,
angle,
true
// fill before stroke
);
}
context.restore();
}
});
};
var renderCropHandles = (context, renderConfig, appState, croppingElement, elementsMap) => {
const [x1, y1, , , cx, cy] = getElementAbsoluteCoords(
croppingElement,
elementsMap
);
const LINE_WIDTH = 3;
const LINE_LENGTH = 20;
const ZOOMED_LINE_WIDTH = LINE_WIDTH / appState.zoom.value;
const ZOOMED_HALF_LINE_WIDTH = ZOOMED_LINE_WIDTH / 2;
const HALF_WIDTH = cx - x1 + ZOOMED_LINE_WIDTH;
const HALF_HEIGHT = cy - y1 + ZOOMED_LINE_WIDTH;
const HORIZONTAL_LINE_LENGTH = Math.min(
LINE_LENGTH / appState.zoom.value,
HALF_WIDTH
);
const VERTICAL_LINE_LENGTH = Math.min(
LINE_LENGTH / appState.zoom.value,
HALF_HEIGHT
);
context.save();
context.fillStyle = renderConfig.selectionColor;
context.strokeStyle = renderConfig.selectionColor;
context.lineWidth = ZOOMED_LINE_WIDTH;
const handles = [
[
// x, y
[-HALF_WIDTH, -HALF_HEIGHT],
// horizontal line: first start and to
[0, ZOOMED_HALF_LINE_WIDTH],
[HORIZONTAL_LINE_LENGTH, ZOOMED_HALF_LINE_WIDTH],
// vertical line: second start and to
[ZOOMED_HALF_LINE_WIDTH, 0],
[ZOOMED_HALF_LINE_WIDTH, VERTICAL_LINE_LENGTH]
],
[
[HALF_WIDTH - ZOOMED_HALF_LINE_WIDTH, -HALF_HEIGHT],
[ZOOMED_HALF_LINE_WIDTH, ZOOMED_HALF_LINE_WIDTH],
[
-HORIZONTAL_LINE_LENGTH + ZOOMED_HALF_LINE_WIDTH,
ZOOMED_HALF_LINE_WIDTH
],
[0, 0],
[0, VERTICAL_LINE_LENGTH]
],
[
[-HALF_WIDTH, HALF_HEIGHT],
[0, -ZOOMED_HALF_LINE_WIDTH],
[HORIZONTAL_LINE_LENGTH, -ZOOMED_HALF_LINE_WIDTH],
[ZOOMED_HALF_LINE_WIDTH, 0],
[ZOOMED_HALF_LINE_WIDTH, -VERTICAL_LINE_LENGTH]
],
[
[HALF_WIDTH - ZOOMED_HALF_LINE_WIDTH, HALF_HEIGHT],
[ZOOMED_HALF_LINE_WIDTH, -ZOOMED_HALF_LINE_WIDTH],
[
-HORIZONTAL_LINE_LENGTH + ZOOMED_HALF_LINE_WIDTH,
-ZOOMED_HALF_LINE_WIDTH
],
[0, 0],
[0, -VERTICAL_LINE_LENGTH]
]
];
handles.forEach((handle) => {
const [[x, y], [x1s, y1s], [x1t, y1t], [x2s, y2s], [x2t, y2t]] = handle;
context.save();
context.translate(cx, cy);
context.rotate(croppingElement.angle);
context.beginPath();
context.moveTo(x + x1s, y + y1s);
context.lineTo(x + x1t, y + y1t);
context.stroke();
context.beginPath();
context.moveTo(x + x2s, y + y2s);
context.lineTo(x + x2t, y + y2t);
context.stroke();
context.restore();
});
context.restore();
};
var renderTextBox = (text, context, appState, selectionColor) => {
context.save();
const padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2 / appState.zoom.value;
const width = text.width + padding * 2;
const height = text.height + padding * 2;
const cx = text.x + width / 2;
const cy = text.y + height / 2;
const shiftX = -(width / 2 + padding);
const shiftY = -(height / 2 + padding);
context.translate(cx + appState.scrollX, cy + appState.scrollY);
context.rotate(text.angle);
context.lineWidth = 1 / appState.zoom.value;
context.strokeStyle = selectionColor;
context.strokeRect(shiftX, shiftY, width, height);
context.restore();
};
var _renderInteractiveScene = ({
canvas,
elementsMap,
visibleElements,
selectedElements,
allElementsMap,
scale,
appState,
renderConfig,
device
}) => {
if (canvas === null) {
return { atLeastOneVisibleElement: false, elementsMap };
}
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
canvas,
scale
);
const context = bootstrapCanvas({
canvas,
scale,
normalizedWidth,
normalizedHeight
});
context.save();
context.scale(appState.zoom.value, appState.zoom.value);
let editingLinearElement = void 0;
visibleElements.forEach((element) => {
if (appState.editingLinearElement?.elementId === element.id) {
if (element) {
editingLinearElement = element;
}
}
});
if (editingLinearElement) {
renderLinearPointHandles(
context,
appState,
editingLinearElement,
elementsMap
);
}
if (appState.selectionElement && !appState.isCropping) {
try {
renderSelectionElement(
appState.selectionElement,
context,
appState,
renderConfig.selectionColor
);
} catch (error) {
console.error(error);
}
}
if (appState.editingTextElement && isTextElement(appState.editingTextElement)) {
const textElement = allElementsMap.get(appState.editingTextElement.id);
if (textElement && !textElement.autoResize) {
renderTextBox(
textElement,
context,
appState,
renderConfig.selectionColor
);
}
}
if (appState.isBindingEnabled) {
appState.suggestedBindings.filter((binding) => binding != null).forEach((suggestedBinding) => {
renderBindingHighlight(
context,
appState,
suggestedBinding,
elementsMap
);
});
}
if (appState.frameToHighlight) {
renderFrameHighlight(
context,
appState,
appState.frameToHighlight,
elementsMap
);
}
if (appState.elementsToHighlight) {
renderElementsBoxHighlight(context, appState, appState.elementsToHighlight);
}
const isFrameSelected = selectedElements.some(
(element) => isFrameLikeElement(element)
);
if (selectedElements.length === 1 && appState.editingLinearElement?.elementId === selectedElements[0].id) {
renderLinearPointHandles(
context,
appState,
selectedElements[0],
elementsMap
);
}
if (isElbowArrow(selectedElements[0]) && appState.selectedLinearElement && appState.selectedLinearElement.segmentMidPointHoveredCoords) {
renderElbowArrowMidPointHighlight(context, appState);
} else if (appState.selectedLinearElement && appState.selectedLinearElement.hoverPointIndex >= 0 && !(isElbowArrow(selectedElements[0]) && appState.selectedLinearElement.hoverPointIndex > 0 && appState.selectedLinearElement.hoverPointIndex < selectedElements[0].points.length - 1)) {
renderLinearElementPointHighlight(context, appState, elementsMap);
}
if (!appState.multiElement && !appState.editingLinearElement) {
const showBoundingBox = shouldShowBoundingBox(selectedElements, appState);
const isSingleLinearElementSelected = selectedElements.length === 1 && isLinearElement(selectedElements[0]);
if (isSingleLinearElementSelected && appState.selectedLinearElement?.elementId === selectedElements[0].id && !selectedElements[0].locked) {
renderLinearPointHandles(
context,
appState,
selectedElements[0],
elementsMap
);
}
const selectionColor = renderConfig.selectionColor || oc2.black;
if (showBoundingBox) {
const locallySelectedIds = arrayToMap(selectedElements);
const selections = [];
for (const element of elementsMap.values()) {
const selectionColors = [];
const remoteClients = renderConfig.remoteSelectedElementIds.get(
element.id
);
if (!// Elbow arrow elements cannot be selected when bound on either end
(isSingleLinearElementSelected && isElbowArrow(element) && (element.startBinding || element.endBinding))) {
if (locallySelectedIds.has(element.id) && !isSelectedViaGroup(appState, element)) {
selectionColors.push(selectionColor);
}
if (remoteClients) {
selectionColors.push(
...remoteClients.map((socketId) => {
const background = getClientColor(
socketId,
appState.collaborators.get(socketId)
);
return background;
})
);
}
}
if (selectionColors.length) {
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
true
);
selections.push({
angle: element.angle,
x1,
y1,
x2,
y2,
selectionColors,
dashed: !!remoteClients,
cx,
cy,
activeEmbeddable: appState.activeEmbeddable?.element === element && appState.activeEmbeddable.state === "active",
padding: element.id === appState.croppingElementId || isImageElement(element) ? 0 : void 0
});
}
}
const addSelectionForGroupId = (groupId) => {
const groupElements = getElementsInGroup(elementsMap, groupId);
const [x1, y1, x2, y2] = getCommonBounds(groupElements);
selections.push({
angle: 0,
x1,
x2,
y1,
y2,
selectionColors: [oc2.black],
dashed: true,
cx: x1 + (x2 - x1) / 2,
cy: y1 + (y2 - y1) / 2,
activeEmbeddable: false
});
};
for (const groupId of getSelectedGroupIds(appState)) {
addSelectionForGroupId(groupId);
}
if (appState.editingGroupId) {
addSelectionForGroupId(appState.editingGroupId);
}
selections.forEach(
(selection) => renderSelectionBorder(context, appState, selection)
);
}
context.save();
context.translate(appState.scrollX, appState.scrollY);
if (selectedElements.length === 1) {
context.fillStyle = oc2.white;
const transformHandles = getTransformHandles(
selectedElements[0],
appState.zoom,
elementsMap,
"mouse",
// when we render we don't know which pointer type so use mouse,
getOmitSidesForDevice(device)
);
if (!appState.viewModeEnabled && showBoundingBox && // do not show transform handles when text is being edited
!isTextElement(appState.editingTextElement) && // do not show transform handles when image is being cropped
!appState.croppingElementId) {
renderTransformHandles(
context,
renderConfig,
appState,
transformHandles,
selectedElements[0].angle
);
}
if (appState.croppingElementId && !appState.isCropping) {
const croppingElement = elementsMap.get(appState.croppingElementId);
if (croppingElement && isImageElement(croppingElement)) {
renderCropHandles(
context,
renderConfig,
appState,
croppingElement,
elementsMap
);
}
}
} else if (selectedElements.length > 1 && !appState.isRotating) {
const dashedLinePadding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2 / appState.zoom.value;
context.fillStyle = oc2.white;
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
const initialLineDash = context.getLineDash();
context.setLineDash([2 / appState.zoom.value]);
const lineWidth = context.lineWidth;
context.lineWidth = 1 / appState.zoom.value;
context.strokeStyle = selectionColor;
strokeRectWithRotation(
context,
x1 - dashedLinePadding,
y1 - dashedLinePadding,
x2 - x1 + dashedLinePadding * 2,
y2 - y1 + dashedLinePadding * 2,
(x1 + x2) / 2,
(y1 + y2) / 2,
0
);
context.lineWidth = lineWidth;
context.setLineDash(initialLineDash);
const transformHandles = getTransformHandlesFromCoords(
[x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2],
0,
appState.zoom,
"mouse",
isFrameSelected ? { ...getOmitSidesForDevice(device), rotation: true } : getOmitSidesForDevice(device)
);
if (selectedElements.some((element) => !element.locked)) {
renderTransformHandles(
context,
renderConfig,
appState,
transformHandles,
0
);
}
}
context.restore();
}
appState.searchMatches.forEach(({ id, focus, matchedLines }) => {
const element = elementsMap.get(id);
if (element && isTextElement(element)) {
const [elementX1, elementY1, , , cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
true
);
context.save();
if (appState.theme === THEME.LIGHT) {
if (focus) {
context.fillStyle = "rgba(255, 124, 0, 0.4)";
} else {
context.fillStyle = "rgba(255, 226, 0, 0.4)";
}
} else if (focus) {
context.fillStyle = "rgba(229, 82, 0, 0.4)";
} else {
context.fillStyle = "rgba(99, 52, 0, 0.4)";
}
context.translate(appState.scrollX, appState.scrollY);
context.translate(cx, cy);
context.rotate(element.angle);
matchedLines.forEach((matchedLine) => {
context.fillRect(
elementX1 + matchedLine.offsetX - cx,
elementY1 + matchedLine.offsetY - cy,
matchedLine.width,
matchedLine.height
);
});
context.restore();
}
});
renderSnaps(context, appState);
context.restore();
renderRemoteCursors({
context,
renderConfig,
appState,
normalizedWidth,
normalizedHeight
});
let scrollBars;
if (renderConfig.renderScrollbars) {
scrollBars = getScrollBars(
visibleElements,
normalizedWidth,
normalizedHeight,
appState
);
context.save();
context.fillStyle = SCROLLBAR_COLOR;
context.strokeStyle = "rgba(255,255,255,0.8)";
[scrollBars.horizontal, scrollBars.vertical].forEach((scrollBar) => {
if (scrollBar) {
roundRect(
context,
scrollBar.x,
scrollBar.y,
scrollBar.width,
scrollBar.height,
SCROLLBAR_WIDTH / 2
);
}
});
context.restore();
}
return {
scrollBars,
atLeastOneVisibleElement: visibleElements.length > 0,
elementsMap
};
};
var renderInteractiveSceneThrottled = throttleRAF(
(config) => {
const ret = _renderInteractiveScene(config);
config.callback?.(ret);
},
{ trailing: true }
);
var renderInteractiveScene = (renderConfig, throttle5) => {
if (throttle5) {
renderInteractiveSceneThrottled(renderConfig);
return void 0;
}
const ret = _renderInteractiveScene(renderConfig);
renderConfig.callback(ret);
return ret;
};
// components/canvases/InteractiveCanvas.tsx
import { jsx as jsx140 } from "react/jsx-runtime";
var InteractiveCanvas = (props) => {
const isComponentMounted = useRef36(false);
useEffect41(() => {
if (!isComponentMounted.current) {
isComponentMounted.current = true;
return;
}
const remotePointerButton = /* @__PURE__ */ new Map();
const remotePointerViewportCoords = /* @__PURE__ */ new Map();
const remoteSelectedElementIds = /* @__PURE__ */ new Map();
const remotePointerUsernames = /* @__PURE__ */ new Map();
const remotePointerUserStates = /* @__PURE__ */ new Map();
props.appState.collaborators.forEach((user, socketId) => {
if (user.selectedElementIds) {
for (const id of Object.keys(user.selectedElementIds)) {
if (!remoteSelectedElementIds.has(id)) {
remoteSelectedElementIds.set(id, []);
}
remoteSelectedElementIds.get(id).push(socketId);
}
}
if (!user.pointer || user.pointer.renderCursor === false) {
return;
}
if (user.username) {
remotePointerUsernames.set(socketId, user.username);
}
if (user.userState) {
remotePointerUserStates.set(socketId, user.userState);
}
remotePointerViewportCoords.set(
socketId,
sceneCoordsToViewportCoords(
{
sceneX: user.pointer.x,
sceneY: user.pointer.y
},
props.appState
)
);
remotePointerButton.set(socketId, user.button);
});
const selectionColor = props.containerRef?.current && getComputedStyle(props.containerRef.current).getPropertyValue(
"--color-selection"
) || "#6965db";
renderInteractiveScene(
{
canvas: props.canvas,
elementsMap: props.elementsMap,
visibleElements: props.visibleElements,
selectedElements: props.selectedElements,
allElementsMap: props.allElementsMap,
scale: window.devicePixelRatio,
appState: props.appState,
renderConfig: {
remotePointerViewportCoords,
remotePointerButton,
remoteSelectedElementIds,
remotePointerUsernames,
remotePointerUserStates,
selectionColor,
renderScrollbars: false
},
device: props.device,
callback: props.renderInteractiveSceneCallback
},
isRenderThrottlingEnabled()
);
});
return /* @__PURE__ */ jsx140(
"canvas",
{
className: "excalidraw__canvas interactive",
style: {
width: props.appState.width,
height: props.appState.height,
cursor: props.appState.viewModeEnabled ? CURSOR_TYPE.GRAB : CURSOR_TYPE.AUTO
},
width: props.appState.width * props.scale,
height: props.appState.height * props.scale,
ref: props.handleCanvasRef,
onContextMenu: props.onContextMenu,
onPointerMove: props.onPointerMove,
onPointerUp: props.onPointerUp,
onPointerCancel: props.onPointerCancel,
onTouchMove: props.onTouchMove,
onPointerDown: props.onPointerDown,
onDoubleClick: props.appState.viewModeEnabled ? void 0 : props.onDoubleClick,
children: t("labels.drawingCanvas")
}
);
};
var getRelevantAppStateProps = (appState) => ({
zoom: appState.zoom,
scrollX: appState.scrollX,
scrollY: appState.scrollY,
width: appState.width,
height: appState.height,
viewModeEnabled: appState.viewModeEnabled,
openDialog: appState.openDialog,
editingGroupId: appState.editingGroupId,
editingLinearElement: appState.editingLinearElement,
selectedElementIds: appState.selectedElementIds,
frameToHighlight: appState.frameToHighlight,
offsetLeft: appState.offsetLeft,
offsetTop: appState.offsetTop,
theme: appState.theme,
pendingImageElementId: appState.pendingImageElementId,
selectionElement: appState.selectionElement,
selectedGroupIds: appState.selectedGroupIds,
selectedLinearElement: appState.selectedLinearElement,
multiElement: appState.multiElement,
isBindingEnabled: appState.isBindingEnabled,
suggestedBindings: appState.suggestedBindings,
isRotating: appState.isRotating,
elementsToHighlight: appState.elementsToHighlight,
collaborators: appState.collaborators,
// Necessary for collab. sessions
activeEmbeddable: appState.activeEmbeddable,
snapLines: appState.snapLines,
zenModeEnabled: appState.zenModeEnabled,
editingTextElement: appState.editingTextElement,
isCropping: appState.isCropping,
croppingElementId: appState.croppingElementId,
searchMatches: appState.searchMatches
});
var areEqual3 = (prevProps, nextProps) => {
if (prevProps.selectionNonce !== nextProps.selectionNonce || prevProps.sceneNonce !== nextProps.sceneNonce || prevProps.scale !== nextProps.scale || // we need to memoize on elementsMap because they may have renewed
// even if sceneNonce didn't change (e.g. we filter elements out based
// on appState)
prevProps.elementsMap !== nextProps.elementsMap || prevProps.visibleElements !== nextProps.visibleElements || prevProps.selectedElements !== nextProps.selectedElements) {
return false;
}
return isShallowEqual(
// asserting AppState because we're being passed the whole AppState
// but resolve to only the InteractiveCanvas-relevant props
getRelevantAppStateProps(prevProps.appState),
getRelevantAppStateProps(nextProps.appState)
);
};
var InteractiveCanvas_default = React41.memo(InteractiveCanvas, areEqual3);
// components/canvases/StaticCanvas.tsx
import React42, { useEffect as useEffect42, useRef as useRef37 } from "react";
import { jsx as jsx141 } from "react/jsx-runtime";
var StaticCanvas = (props) => {
const wrapperRef = useRef37(null);
const isComponentMounted = useRef37(false);
useEffect42(() => {
const wrapper = wrapperRef.current;
if (!wrapper) {
return;
}
const canvas = props.canvas;
if (!isComponentMounted.current) {
isComponentMounted.current = true;
wrapper.replaceChildren(canvas);
canvas.classList.add("excalidraw__canvas", "static");
}
const widthString = `${props.appState.width}px`;
const heightString = `${props.appState.height}px`;
if (canvas.style.width !== widthString) {
canvas.style.width = widthString;
}
if (canvas.style.height !== heightString) {
canvas.style.height = heightString;
}
const scaledWidth = props.appState.width * props.scale;
const scaledHeight = props.appState.height * props.scale;
if (canvas.width !== scaledWidth) {
canvas.width = scaledWidth;
}
if (canvas.height !== scaledHeight) {
canvas.height = scaledHeight;
}
renderStaticScene(
{
canvas,
rc: props.rc,
scale: props.scale,
elementsMap: props.elementsMap,
allElementsMap: props.allElementsMap,
visibleElements: props.visibleElements,
appState: props.appState,
renderConfig: props.renderConfig
},
isRenderThrottlingEnabled()
);
});
return /* @__PURE__ */ jsx141("div", { className: "excalidraw__canvas-wrapper", ref: wrapperRef });
};
var getRelevantAppStateProps2 = (appState) => ({
zoom: appState.zoom,
scrollX: appState.scrollX,
scrollY: appState.scrollY,
width: appState.width,
height: appState.height,
viewModeEnabled: appState.viewModeEnabled,
openDialog: appState.openDialog,
hoveredElementIds: appState.hoveredElementIds,
offsetLeft: appState.offsetLeft,
offsetTop: appState.offsetTop,
theme: appState.theme,
pendingImageElementId: appState.pendingImageElementId,
shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
viewBackgroundColor: appState.viewBackgroundColor,
exportScale: appState.exportScale,
selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged,
gridSize: appState.gridSize,
gridStep: appState.gridStep,
frameRendering: appState.frameRendering,
selectedElementIds: appState.selectedElementIds,
frameToHighlight: appState.frameToHighlight,
editingGroupId: appState.editingGroupId,
currentHoveredFontFamily: appState.currentHoveredFontFamily,
croppingElementId: appState.croppingElementId
});
var areEqual4 = (prevProps, nextProps) => {
if (prevProps.sceneNonce !== nextProps.sceneNonce || prevProps.scale !== nextProps.scale || // we need to memoize on elementsMap because they may have renewed
// even if sceneNonce didn't change (e.g. we filter elements out based
// on appState)
prevProps.elementsMap !== nextProps.elementsMap || prevProps.visibleElements !== nextProps.visibleElements) {
return false;
}
return isShallowEqual(
// asserting AppState because we're being passed the whole AppState
// but resolve to only the StaticCanvas-relevant props
getRelevantAppStateProps2(prevProps.appState),
getRelevantAppStateProps2(nextProps.appState)
) && isShallowEqual(prevProps.renderConfig, nextProps.renderConfig);
};
var StaticCanvas_default = React42.memo(StaticCanvas, areEqual4);
// scene/Renderer.ts
var Renderer = class {
constructor(scene) {
__publicField(this, "scene");
__publicField(this, "getRenderableElements", (() => {
const getVisibleCanvasElements = ({
elementsMap,
zoom,
offsetLeft,
offsetTop,
scrollX,
scrollY,
height,
width
}) => {
const visibleElements = [];
for (const element of elementsMap.values()) {
if (isElementInViewport(
element,
width,
height,
{
zoom,
offsetLeft,
offsetTop,
scrollX,
scrollY
},
elementsMap
)) {
visibleElements.push(element);
}
}
return visibleElements;
};
const getRenderableElements = ({
elements,
editingTextElement,
newElementId,
pendingImageElementId
}) => {
const elementsMap = toBrandedType(/* @__PURE__ */ new Map());
for (const element of elements) {
if (isImageElement(element)) {
if (
// => not placed on canvas yet (but in elements array)
pendingImageElementId === element.id
) {
continue;
}
}
if (newElementId === element.id) {
continue;
}
if (!editingTextElement || editingTextElement.type !== "text" || element.id !== editingTextElement.id) {
elementsMap.set(element.id, element);
}
}
return elementsMap;
};
return memoize(
({
zoom,
offsetLeft,
offsetTop,
scrollX,
scrollY,
height,
width,
editingTextElement,
newElementId,
pendingImageElementId,
// cache-invalidation nonce
sceneNonce: _sceneNonce
}) => {
const elements = this.scene.getNonDeletedElements();
const elementsMap = getRenderableElements({
elements,
editingTextElement,
newElementId,
pendingImageElementId
});
const visibleElements = getVisibleCanvasElements({
elementsMap,
zoom,
offsetLeft,
offsetTop,
scrollX,
scrollY,
height,
width
});
return { elementsMap, visibleElements };
}
);
})());
this.scene = scene;
}
// NOTE Doesn't destroy everything (scene, rc, etc.) because it may not be
// safe to break TS contract here (for upstream cases)
destroy() {
renderInteractiveSceneThrottled.cancel();
renderStaticSceneThrottled.cancel();
this.getRenderableElements.clear();
}
};
// components/SVGLayer.tsx
import { useEffect as useEffect43, useRef as useRef38 } from "react";
import { jsx as jsx142 } from "react/jsx-runtime";
var SVGLayer = ({ trails }) => {
const svgRef = useRef38(null);
useEffect43(() => {
if (svgRef.current) {
for (const trail of trails) {
trail.start(svgRef.current);
}
}
return () => {
for (const trail of trails) {
trail.stop();
}
};
}, trails);
return /* @__PURE__ */ jsx142("div", { className: "SVGLayer", children: /* @__PURE__ */ jsx142("svg", { ref: svgRef }) });
};
// element/ElementCanvasButtons.tsx
import { jsx as jsx143 } from "react/jsx-runtime";
var CONTAINER_PADDING = 5;
var getContainerCoords = (element, appState, elementsMap) => {
const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width, sceneY: y1 },
appState
);
const x = viewportX - appState.offsetLeft + 10;
const y = viewportY - appState.offsetTop;
return { x, y };
};
var ElementCanvasButtons = ({
children,
element,
elementsMap
}) => {
const appState = useExcalidrawAppState();
if (appState.contextMenu || appState.newElement || appState.resizingElement || appState.isRotating || appState.openMenu || appState.viewModeEnabled) {
return null;
}
const { x, y } = getContainerCoords(element, appState, elementsMap);
return /* @__PURE__ */ jsx143(
"div",
{
className: "excalidraw-canvas-buttons",
style: {
top: `${y}px`,
left: `${x}px`,
// width: CONTAINER_WIDTH,
padding: CONTAINER_PADDING
},
children
}
);
};
// components/MagicButton.tsx
import clsx54 from "clsx";
import { jsx as jsx144, jsxs as jsxs76 } from "react/jsx-runtime";
var DEFAULT_SIZE4 = "small";
var ElementCanvasButton = (props) => {
return /* @__PURE__ */ jsxs76(
"label",
{
className: clsx54(
"ToolIcon ToolIcon__MagicButton",
`ToolIcon_size_${DEFAULT_SIZE4}`,
{
"is-mobile": props.isMobile
}
),
title: `${props.title}`,
children: [
/* @__PURE__ */ jsx144(
"input",
{
className: "ToolIcon_type_checkbox",
type: "checkbox",
name: props.name,
onChange: props.onChange,
checked: props.checked,
"aria-label": props.title
}
),
/* @__PURE__ */ jsx144("div", { className: "ToolIcon__icon", children: props.icon })
]
}
);
};
// components/FollowMode/FollowMode.tsx
import { jsx as jsx145, jsxs as jsxs77 } from "react/jsx-runtime";
var FollowMode = ({
height,
width,
userToFollow,
onDisconnect
}) => {
return /* @__PURE__ */ jsx145("div", { className: "follow-mode", style: { width, height }, children: /* @__PURE__ */ jsxs77("div", { className: "follow-mode__badge", children: [
/* @__PURE__ */ jsxs77("div", { className: "follow-mode__badge__label", children: [
"Following",
" ",
/* @__PURE__ */ jsx145(
"span",
{
className: "follow-mode__badge__username",
title: userToFollow.username,
children: userToFollow.username
}
)
] }),
/* @__PURE__ */ jsx145(
"button",
{
type: "button",
onClick: onDisconnect,
className: "follow-mode__disconnect-btn",
children: CloseIcon
}
)
] }) });
};
var FollowMode_default = FollowMode;
// animation-frame-handler.ts
var AnimationFrameHandler = class {
constructor() {
__publicField(this, "targets", /* @__PURE__ */ new WeakMap());
__publicField(this, "rafIds", /* @__PURE__ */ new WeakMap());
}
register(key, callback) {
this.targets.set(key, { callback, stopped: true });
}
start(key) {
const target = this.targets.get(key);
if (!target) {
return;
}
if (this.rafIds.has(key)) {
return;
}
this.targets.set(key, { ...target, stopped: false });
this.scheduleFrame(key);
}
stop(key) {
const target = this.targets.get(key);
if (target && !target.stopped) {
this.targets.set(key, { ...target, stopped: true });
}
this.cancelFrame(key);
}
constructFrame(key) {
return (timestamp) => {
const target = this.targets.get(key);
if (!target) {
return;
}
const shouldAbort = this.onFrame(target, timestamp);
if (!target.stopped && !shouldAbort) {
this.scheduleFrame(key);
} else {
this.cancelFrame(key);
}
};
}
scheduleFrame(key) {
const rafId = requestAnimationFrame(this.constructFrame(key));
this.rafIds.set(key, rafId);
}
cancelFrame(key) {
if (this.rafIds.has(key)) {
const rafId = this.rafIds.get(key);
cancelAnimationFrame(rafId);
}
this.rafIds.delete(key);
}
onFrame(target, timestamp) {
const shouldAbort = target.callback(timestamp);
return shouldAbort ?? false;
}
};
// animated-trail.ts
import { LaserPointer } from "@excalidraw/laser-pointer";
var AnimatedTrail = class {
constructor(animationFrameHandler, app, options) {
this.animationFrameHandler = animationFrameHandler;
this.app = app;
this.options = options;
__publicField(this, "currentTrail");
__publicField(this, "pastTrails", []);
__publicField(this, "container");
__publicField(this, "trailElement");
this.animationFrameHandler.register(this, this.onFrame.bind(this));
this.trailElement = document.createElementNS(SVG_NS, "path");
}
get hasCurrentTrail() {
return !!this.currentTrail;
}
hasLastPoint(x, y) {
if (this.currentTrail) {
const len = this.currentTrail.originalPoints.length;
return this.currentTrail.originalPoints[len - 1][0] === x && this.currentTrail.originalPoints[len - 1][1] === y;
}
return false;
}
start(container) {
if (container) {
this.container = container;
}
if (this.trailElement.parentNode !== this.container && this.container) {
this.container.appendChild(this.trailElement);
}
this.animationFrameHandler.start(this);
}
stop() {
this.animationFrameHandler.stop(this);
if (this.trailElement.parentNode === this.container) {
this.container?.removeChild(this.trailElement);
}
}
startPath(x, y) {
this.currentTrail = new LaserPointer(this.options);
this.currentTrail.addPoint([x, y, performance.now()]);
this.update();
}
addPointToPath(x, y) {
if (this.currentTrail) {
this.currentTrail.addPoint([x, y, performance.now()]);
this.update();
}
}
endPath() {
if (this.currentTrail) {
this.currentTrail.close();
this.currentTrail.options.keepHead = false;
this.pastTrails.push(this.currentTrail);
this.currentTrail = void 0;
this.update();
}
}
update() {
this.start();
}
onFrame() {
const paths = [];
for (const trail of this.pastTrails) {
paths.push(this.drawTrail(trail, this.app.state));
}
if (this.currentTrail) {
const currentPath = this.drawTrail(this.currentTrail, this.app.state);
paths.push(currentPath);
}
this.pastTrails = this.pastTrails.filter((trail) => {
return trail.getStrokeOutline().length !== 0;
});
if (paths.length === 0) {
this.stop();
}
const svgPaths = paths.join(" ").trim();
this.trailElement.setAttribute("d", svgPaths);
this.trailElement.setAttribute(
"fill",
(this.options.fill ?? (() => "black"))(this)
);
}
drawTrail(trail, state) {
const stroke = trail.getStrokeOutline(trail.options.size / state.zoom.value).map(([x, y]) => {
const result = sceneCoordsToViewportCoords(
{ sceneX: x, sceneY: y },
state
);
return [result.x, result.y];
});
return getSvgPathFromStroke(stroke, true);
}
};
// laser-trails.ts
var LaserTrails = class {
constructor(animationFrameHandler, app) {
this.animationFrameHandler = animationFrameHandler;
this.app = app;
__publicField(this, "localTrail");
__publicField(this, "collabTrails", /* @__PURE__ */ new Map());
__publicField(this, "container");
this.animationFrameHandler.register(this, this.onFrame.bind(this));
this.localTrail = new AnimatedTrail(animationFrameHandler, app, {
...this.getTrailOptions(),
fill: () => DEFAULT_LASER_COLOR
});
}
getTrailOptions() {
return {
simplify: 0,
streamline: 0.4,
sizeMapping: (c) => {
const DECAY_TIME = 1e3;
const DECAY_LENGTH = 50;
const t2 = Math.max(
0,
1 - (performance.now() - c.pressure) / DECAY_TIME
);
const l = (DECAY_LENGTH - Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) / DECAY_LENGTH;
return Math.min(easeOut(l), easeOut(t2));
}
};
}
startPath(x, y) {
this.localTrail.startPath(x, y);
}
addPointToPath(x, y) {
this.localTrail.addPointToPath(x, y);
}
endPath() {
this.localTrail.endPath();
}
start(container) {
this.container = container;
this.animationFrameHandler.start(this);
this.localTrail.start(container);
}
stop() {
this.animationFrameHandler.stop(this);
this.localTrail.stop();
}
onFrame() {
this.updateCollabTrails();
}
updateCollabTrails() {
if (!this.container || this.app.state.collaborators.size === 0) {
return;
}
for (const [key, collaborator] of this.app.state.collaborators.entries()) {
let trail;
if (!this.collabTrails.has(key)) {
trail = new AnimatedTrail(this.animationFrameHandler, this.app, {
...this.getTrailOptions(),
fill: () => collaborator.pointer?.laserColor || getClientColor(key, collaborator)
});
trail.start(this.container);
this.collabTrails.set(key, trail);
} else {
trail = this.collabTrails.get(key);
}
if (collaborator.pointer && collaborator.pointer.tool === "laser") {
if (collaborator.button === "down" && !trail.hasCurrentTrail) {
trail.startPath(collaborator.pointer.x, collaborator.pointer.y);
}
if (collaborator.button === "down" && trail.hasCurrentTrail && !trail.hasLastPoint(collaborator.pointer.x, collaborator.pointer.y)) {
trail.addPointToPath(collaborator.pointer.x, collaborator.pointer.y);
}
if (collaborator.button === "up" && trail.hasCurrentTrail) {
trail.addPointToPath(collaborator.pointer.x, collaborator.pointer.y);
trail.endPath();
}
}
}
for (const key of this.collabTrails.keys()) {
if (!this.app.state.collaborators.has(key)) {
const trail = this.collabTrails.get(key);
trail.stop();
this.collabTrails.delete(key);
}
}
}
};
// element/textWysiwyg.tsx
var getTransform = (width, height, angle, appState, maxWidth, maxHeight) => {
const { zoom } = appState;
const degree = 180 * angle / Math.PI;
let translateX = width * (zoom.value - 1) / 2;
let translateY = height * (zoom.value - 1) / 2;
if (width > maxWidth && zoom.value !== 1) {
translateX = maxWidth * (zoom.value - 1) / 2;
}
if (height > maxHeight && zoom.value !== 1) {
translateY = maxHeight * (zoom.value - 1) / 2;
}
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
};
var textWysiwyg = ({
id,
onChange,
onSubmit,
getViewportCoords,
element,
canvas,
excalidrawContainer,
app,
autoSelect = true
}) => {
const textPropertiesUpdated = (updatedTextElement, editable2) => {
if (!editable2.style.fontFamily || !editable2.style.fontSize) {
return false;
}
const currentFont = editable2.style.fontFamily.replace(/"/g, "");
if (getFontFamilyString({ fontFamily: updatedTextElement.fontFamily }) !== currentFont) {
return true;
}
if (`${updatedTextElement.fontSize}px` !== editable2.style.fontSize) {
return true;
}
return false;
};
const updateWysiwygStyle = () => {
const appState = app.state;
const updatedTextElement = Scene_default.getScene(element)?.getElement(id);
if (!updatedTextElement) {
return;
}
const { textAlign, verticalAlign } = updatedTextElement;
const elementsMap = app.scene.getNonDeletedElementsMap();
if (updatedTextElement && isTextElement(updatedTextElement)) {
let coordX = updatedTextElement.x;
let coordY = updatedTextElement.y;
const container = getContainerElement(
updatedTextElement,
app.scene.getNonDeletedElementsMap()
);
let width = updatedTextElement.width;
let height = updatedTextElement.height;
let maxWidth = updatedTextElement.width;
let maxHeight = updatedTextElement.height;
if (container && updatedTextElement.containerId) {
if (isArrowElement(container)) {
const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(
container,
updatedTextElement,
elementsMap
);
coordX = boundTextCoords.x;
coordY = boundTextCoords.y;
}
const propertiesUpdated = textPropertiesUpdated(
updatedTextElement,
editable
);
let originalContainerData;
if (propertiesUpdated) {
originalContainerData = updateOriginalContainerCache(
container.id,
container.height
);
} else {
originalContainerData = originalContainerCache[container.id];
if (!originalContainerData) {
originalContainerData = updateOriginalContainerCache(
container.id,
container.height
);
}
}
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
maxHeight = getBoundTextMaxHeight(
container,
updatedTextElement
);
if (!isArrowElement(container) && height > maxHeight) {
const targetContainerHeight = computeContainerDimensionForBoundText(
height,
container.type
);
mutateElement(container, { height: targetContainerHeight });
return;
} else if (
// autoshrink container height until original container height
// is reached when text is removed
!isArrowElement(container) && container.height > originalContainerData.height && height < maxHeight
) {
const targetContainerHeight = computeContainerDimensionForBoundText(
height,
container.type
);
mutateElement(container, { height: targetContainerHeight });
} else {
const { y } = computeBoundTextPosition(
container,
updatedTextElement,
elementsMap
);
coordY = y;
}
}
const [viewportX, viewportY] = getViewportCoords(coordX, coordY);
const initialSelectionStart = editable.selectionStart;
const initialSelectionEnd = editable.selectionEnd;
const initialLength = editable.value.length;
if (initialSelectionStart === initialSelectionEnd && initialSelectionEnd !== initialLength) {
const diff = initialLength - initialSelectionEnd;
editable.selectionStart = editable.value.length - diff;
editable.selectionEnd = editable.value.length - diff;
}
if (!container) {
maxWidth = (appState.width - 8 - viewportX) / appState.zoom.value;
width = Math.min(width, maxWidth);
} else {
width += 0.5;
}
height *= 1.05;
const font = getFontString(updatedTextElement);
const editorMaxHeight = (appState.height - viewportY) / appState.zoom.value;
Object.assign(editable.style, {
font,
// must be defined *after* font ¯\_(ツ)_/¯
lineHeight: updatedTextElement.lineHeight,
width: `${width}px`,
height: `${height}px`,
left: `${viewportX}px`,
top: `${viewportY}px`,
transform: getTransform(
width,
height,
getTextElementAngle(updatedTextElement, container),
appState,
maxWidth,
editorMaxHeight
),
textAlign,
verticalAlign,
color: updatedTextElement.strokeColor,
opacity: updatedTextElement.opacity / 100,
filter: "var(--theme-filter)",
maxHeight: `${editorMaxHeight}px`
});
editable.scrollTop = 0;
if (isTestEnv()) {
editable.style.fontFamily = getFontFamilyString(updatedTextElement);
}
mutateElement(updatedTextElement, { x: coordX, y: coordY });
}
};
const editable = document.createElement("textarea");
editable.dir = "auto";
editable.tabIndex = 0;
editable.dataset.type = "wysiwyg";
editable.wrap = "off";
editable.classList.add("excalidraw-wysiwyg");
let whiteSpace = "pre";
let wordBreak = "normal";
if (isBoundToContainer(element) || !element.autoResize) {
whiteSpace = "pre-wrap";
wordBreak = "break-word";
}
Object.assign(editable.style, {
position: "absolute",
display: "inline-block",
minHeight: "1em",
backfaceVisibility: "hidden",
margin: 0,
padding: 0,
border: 0,
outline: 0,
resize: "none",
background: "transparent",
overflow: "hidden",
// must be specified because in dark mode canvas creates a stacking context
zIndex: "var(--zIndex-wysiwyg)",
wordBreak,
// prevent line wrapping (`whitespace: nowrap` doesn't work on FF)
whiteSpace,
overflowWrap: "break-word",
boxSizing: "content-box"
});
editable.value = element.originalText;
updateWysiwygStyle();
if (onChange) {
editable.onpaste = async (event) => {
const clipboardData = await parseClipboard(event, true);
if (!clipboardData.text) {
return;
}
const data = normalizeText(clipboardData.text);
if (!data) {
return;
}
const container = getContainerElement(
element,
app.scene.getNonDeletedElementsMap()
);
const font = getFontString({
fontSize: app.state.currentItemFontSize,
fontFamily: app.state.currentItemFontFamily
});
if (container) {
const boundTextElement = getBoundTextElement(
container,
app.scene.getNonDeletedElementsMap()
);
const wrappedText = wrapText(
`${editable.value}${data}`,
font,
getBoundTextMaxWidth(container, boundTextElement)
);
const width = getTextWidth(wrappedText, font);
editable.style.width = `${width}px`;
}
};
editable.oninput = () => {
const normalized = normalizeText(editable.value);
if (editable.value !== normalized) {
const selectionStart = editable.selectionStart;
editable.value = normalized;
editable.selectionStart = selectionStart;
editable.selectionEnd = selectionStart;
}
onChange(editable.value);
};
}
editable.onkeydown = (event) => {
if (!event.shiftKey && actionZoomIn.keyTest(event)) {
event.preventDefault();
app.actionManager.executeAction(actionZoomIn);
updateWysiwygStyle();
} else if (!event.shiftKey && actionZoomOut.keyTest(event)) {
event.preventDefault();
app.actionManager.executeAction(actionZoomOut);
updateWysiwygStyle();
} else if (!event.shiftKey && actionResetZoom.keyTest(event)) {
event.preventDefault();
app.actionManager.executeAction(actionResetZoom);
updateWysiwygStyle();
} else if (actionDecreaseFontSize.keyTest(event)) {
app.actionManager.executeAction(actionDecreaseFontSize);
} else if (actionIncreaseFontSize.keyTest(event)) {
app.actionManager.executeAction(actionIncreaseFontSize);
} else if (event.key === KEYS.ESCAPE) {
event.preventDefault();
submittedViaKeyboard = true;
handleSubmit();
} else if (event.key === KEYS.ENTER && event[KEYS.CTRL_OR_CMD]) {
event.preventDefault();
if (event.isComposing || event.keyCode === 229) {
return;
}
submittedViaKeyboard = true;
handleSubmit();
} else if (event.key === KEYS.TAB || event[KEYS.CTRL_OR_CMD] && (event.code === CODES.BRACKET_LEFT || event.code === CODES.BRACKET_RIGHT)) {
event.preventDefault();
if (event.isComposing) {
return;
} else if (event.shiftKey || event.code === CODES.BRACKET_LEFT) {
outdent();
} else {
indent();
}
editable.dispatchEvent(new Event("input"));
}
};
const TAB_SIZE = 4;
const TAB = " ".repeat(TAB_SIZE);
const RE_LEADING_TAB = new RegExp(`^ {1,${TAB_SIZE}}`);
const indent = () => {
const { selectionStart, selectionEnd } = editable;
const linesStartIndices = getSelectedLinesStartIndices();
let value = editable.value;
linesStartIndices.forEach((startIndex) => {
const startValue = value.slice(0, startIndex);
const endValue = value.slice(startIndex);
value = `${startValue}${TAB}${endValue}`;
});
editable.value = value;
editable.selectionStart = selectionStart + TAB_SIZE;
editable.selectionEnd = selectionEnd + TAB_SIZE * linesStartIndices.length;
};
const outdent = () => {
const { selectionStart, selectionEnd } = editable;
const linesStartIndices = getSelectedLinesStartIndices();
const removedTabs = [];
let value = editable.value;
linesStartIndices.forEach((startIndex) => {
const tabMatch = value.slice(startIndex, startIndex + TAB_SIZE).match(RE_LEADING_TAB);
if (tabMatch) {
const startValue = value.slice(0, startIndex);
const endValue = value.slice(startIndex + tabMatch[0].length);
value = `${startValue}${endValue}`;
removedTabs.push(startIndex);
}
});
editable.value = value;
if (removedTabs.length) {
if (selectionStart > removedTabs[removedTabs.length - 1]) {
editable.selectionStart = Math.max(
selectionStart - TAB_SIZE,
removedTabs[removedTabs.length - 1]
);
} else {
editable.selectionStart = selectionStart;
}
editable.selectionEnd = Math.max(
editable.selectionStart,
selectionEnd - TAB_SIZE * removedTabs.length
);
}
};
const getSelectedLinesStartIndices = () => {
let { selectionStart, selectionEnd, value } = editable;
const startOffset = value.slice(0, selectionStart).match(/[^\n]*$/)[0].length;
selectionStart = selectionStart - startOffset;
const selected = value.slice(selectionStart, selectionEnd);
return selected.split("\n").reduce(
(startIndices, line, idx, lines) => startIndices.concat(
idx ? (
// curr line index is prev line's start + prev line's length + \n
startIndices[idx - 1] + lines[idx - 1].length + 1
) : (
// first selected line
selectionStart
)
),
[]
).reverse();
};
const stopEvent = (event) => {
if (event.target instanceof HTMLCanvasElement) {
event.preventDefault();
event.stopPropagation();
}
};
let submittedViaKeyboard = false;
const handleSubmit = () => {
if (isDestroyed) {
return;
}
isDestroyed = true;
cleanup();
const updateElement = Scene_default.getScene(element)?.getElement(
element.id
);
if (!updateElement) {
return;
}
const container = getContainerElement(
updateElement,
app.scene.getNonDeletedElementsMap()
);
if (container) {
if (editable.value.trim()) {
const boundTextElementId = getBoundTextElementId(container);
if (!boundTextElementId || boundTextElementId !== element.id) {
mutateElement(container, {
boundElements: (container.boundElements || []).concat({
type: "text",
id: element.id
})
});
} else if (isArrowElement(container)) {
bumpVersion(container);
}
} else {
mutateElement(container, {
boundElements: container.boundElements?.filter(
(ele) => !isTextElement(
ele
)
)
});
}
redrawTextBoundingBox(
updateElement,
container,
app.scene.getNonDeletedElementsMap()
);
}
onSubmit({
viaKeyboard: submittedViaKeyboard,
nextOriginalText: editable.value
});
};
const cleanup = () => {
editable.onblur = null;
editable.oninput = null;
editable.onkeydown = 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);
unbindUpdate();
unbindOnScroll();
editable.remove();
};
const bindBlurEvent = (event) => {
window.removeEventListener("pointerup", bindBlurEvent);
const target = event?.target;
const isPropertiesTrigger = target instanceof HTMLElement && target.classList.contains("properties-trigger");
setTimeout(() => {
editable.onblur = handleSubmit;
if (!isPropertiesTrigger) {
editable.focus();
}
});
};
const temporarilyDisableSubmit = () => {
editable.onblur = null;
window.addEventListener("pointerup", bindBlurEvent);
window.addEventListener("blur", handleSubmit);
};
const onPointerDown = (event) => {
const target = event?.target;
if (event.button === POINTER_BUTTON.WHEEL) {
if (target instanceof HTMLTextAreaElement) {
event.preventDefault();
app.handleCanvasPanUsingWheelOrSpaceDrag(event);
}
temporarilyDisableSubmit();
return;
}
const isPropertiesTrigger = target instanceof HTMLElement && target.classList.contains("properties-trigger");
if ((event.target instanceof HTMLElement || event.target instanceof SVGElement) && event.target.closest(
`.${CLASSES.SHAPE_ACTIONS_MENU}, .${CLASSES.ZOOM_ACTIONS}`
) && !isWritableElement(event.target) || isPropertiesTrigger) {
temporarilyDisableSubmit();
} else if (event.target instanceof HTMLCanvasElement && // Vitest simply ignores stopPropagation, capture-mode, or rAF
// so without introducing crazier hacks, nothing we can do
!isTestEnv()) {
requestAnimationFrame(() => {
handleSubmit();
});
}
};
const unbindUpdate = app.scene.onUpdate(() => {
updateWysiwygStyle();
const isPopupOpened = !!document.activeElement?.closest(
".properties-content"
);
if (!isPopupOpened) {
editable.focus();
}
});
const unbindOnScroll = app.onScrollChangeEmitter.on(() => {
updateWysiwygStyle();
});
let isDestroyed = false;
if (autoSelect) {
editable.select();
}
bindBlurEvent();
let observer = null;
if (canvas && "ResizeObserver" in window) {
observer = new window.ResizeObserver(() => {
updateWysiwygStyle();
});
observer.observe(canvas);
} else {
window.addEventListener("resize", updateWysiwygStyle);
}
editable.onpointerdown = (event) => event.stopPropagation();
requestAnimationFrame(() => {
window.addEventListener("pointerdown", onPointerDown, { capture: true });
});
window.addEventListener("beforeunload", handleSubmit);
excalidrawContainer?.querySelector(".excalidraw-textEditorContainer").appendChild(editable);
};
// actions/actionTextAutoResize.ts
var actionTextAutoResize = register({
name: "autoResize",
label: "labels.autoResize",
icon: null,
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length === 1 && isTextElement(selectedElements[0]) && !selectedElements[0].autoResize;
},
perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState);
return {
appState,
elements: elements.map((element) => {
if (element.id === selectedElements[0].id && isTextElement(element)) {
const metrics = measureText(
element.originalText,
getFontString(element),
element.lineHeight
);
return newElementWith(element, {
autoResize: true,
width: metrics.width,
height: metrics.height,
text: element.originalText
});
}
return element;
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
};
}
});
// mermaid.ts
var isMaybeMermaidDefinition = (text) => {
const chartTypes = [
"flowchart",
"graph",
"sequenceDiagram",
"classDiagram",
"stateDiagram",
"stateDiagram-v2",
"erDiagram",
"journey",
"gantt",
"pie",
"quadrantChart",
"requirementDiagram",
"gitGraph",
"C4Context",
"mindmap",
"timeline",
"zenuml",
"sankey",
"xychart",
"block"
];
const re = new RegExp(
`^(?:%%{.*?}%%[\\s\\n]*)?\\b(?:${chartTypes.map((x) => `\\s*${x}(-beta)?`).join("|")})\\b`
);
return re.test(text.trim());
};
// components/canvases/NewElementCanvas.tsx
import { useEffect as useEffect44, useRef as useRef39 } from "react";
// renderer/renderNewElementScene.ts
var _renderNewElementScene = ({
canvas,
rc,
newElement: newElement2,
elementsMap,
allElementsMap,
scale,
appState,
renderConfig
}) => {
if (canvas) {
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
canvas,
scale
);
const context = bootstrapCanvas({
canvas,
scale,
normalizedWidth,
normalizedHeight
});
context.save();
context.scale(appState.zoom.value, appState.zoom.value);
if (newElement2 && newElement2.type !== "selection") {
renderElement(
newElement2,
elementsMap,
allElementsMap,
rc,
context,
renderConfig,
appState
);
} else {
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
}
}
};
var renderNewElementSceneThrottled = throttleRAF(
(config) => {
_renderNewElementScene(config);
},
{ trailing: true }
);
var renderNewElementScene = (renderConfig, throttle5) => {
if (throttle5) {
renderNewElementSceneThrottled(renderConfig);
return;
}
_renderNewElementScene(renderConfig);
};
// components/canvases/NewElementCanvas.tsx
import { jsx as jsx146 } from "react/jsx-runtime";
var NewElementCanvas = (props) => {
const canvasRef = useRef39(null);
useEffect44(() => {
if (!canvasRef.current) {
return;
}
renderNewElementScene(
{
canvas: canvasRef.current,
scale: props.scale,
newElement: props.appState.newElement,
elementsMap: props.elementsMap,
allElementsMap: props.allElementsMap,
rc: props.rc,
renderConfig: props.renderConfig,
appState: props.appState
},
isRenderThrottlingEnabled()
);
});
return /* @__PURE__ */ jsx146(
"canvas",
{
className: "excalidraw__canvas",
style: {
width: props.appState.width,
height: props.appState.height
},
width: props.appState.width * props.scale,
height: props.appState.height * props.scale,
ref: canvasRef
}
);
};
var NewElementCanvas_default = NewElementCanvas;
// components/App.tsx
import { Fragment as Fragment23, jsx as jsx147, jsxs as jsxs78 } from "react/jsx-runtime";
var AppContext = React43.createContext(null);
var AppPropsContext = React43.createContext(null);
var deviceContextInitialValue = {
viewport: {
isMobile: false,
isLandscape: false
},
editor: {
isMobile: false,
canFitSidebar: false
},
isTouchScreen: false
};
var DeviceContext = React43.createContext(deviceContextInitialValue);
DeviceContext.displayName = "DeviceContext";
var ExcalidrawContainerContext = React43.createContext({ container: null, id: null });
ExcalidrawContainerContext.displayName = "ExcalidrawContainerContext";
var ExcalidrawElementsContext = React43.createContext([]);
ExcalidrawElementsContext.displayName = "ExcalidrawElementsContext";
var ExcalidrawAppStateContext = React43.createContext({
...getDefaultAppState(),
width: 0,
height: 0,
offsetLeft: 0,
offsetTop: 0
});
ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext";
var ExcalidrawSetAppStateContext = React43.createContext(() => {
console.warn("Uninitialized ExcalidrawSetAppStateContext context!");
});
ExcalidrawSetAppStateContext.displayName = "ExcalidrawSetAppStateContext";
var ExcalidrawActionManagerContext = React43.createContext(
null
);
ExcalidrawActionManagerContext.displayName = "ExcalidrawActionManagerContext";
var useApp = () => useContext3(AppContext);
var useAppProps = () => useContext3(AppPropsContext);
var useDevice = () => useContext3(DeviceContext);
var useExcalidrawContainer = () => useContext3(ExcalidrawContainerContext);
var useExcalidrawElements = () => useContext3(ExcalidrawElementsContext);
var useExcalidrawAppState = () => useContext3(ExcalidrawAppStateContext);
var useExcalidrawSetAppState = () => useContext3(ExcalidrawSetAppStateContext);
var useExcalidrawActionManager = () => useContext3(ExcalidrawActionManagerContext);
var didTapTwice = false;
var tappedTwiceTimer = 0;
var isHoldingSpace = false;
var isPanning = false;
var isDraggingScrollBar = false;
var currentScrollBars = { horizontal: null, vertical: null };
var touchTimeout = 0;
var invalidateContextMenu = false;
var YOUTUBE_VIDEO_STATES = /* @__PURE__ */ new Map();
var IS_PLAIN_PASTE = false;
var IS_PLAIN_PASTE_TIMER = 0;
var PLAIN_PASTE_TOAST_SHOWN = false;
var lastPointerUp = null;
var gesture = {
pointers: /* @__PURE__ */ new Map(),
lastCenter: null,
initialDistance: null,
initialScale: null
};
var App = class _App extends React43.Component {
constructor(props) {
super(props);
__publicField(this, "canvas");
__publicField(this, "interactiveCanvas", null);
__publicField(this, "rc");
__publicField(this, "unmounted", false);
__publicField(this, "actionManager");
__publicField(this, "device", deviceContextInitialValue);
__publicField(this, "excalidrawContainerRef", React43.createRef());
__publicField(this, "scene");
__publicField(this, "fonts");
__publicField(this, "renderer");
__publicField(this, "visibleElements");
__publicField(this, "resizeObserver");
__publicField(this, "nearestScrollableContainer");
__publicField(this, "library");
__publicField(this, "libraryItemsFromStorage");
__publicField(this, "id");
__publicField(this, "store");
__publicField(this, "history");
__publicField(this, "excalidrawContainerValue");
__publicField(this, "files", {});
__publicField(this, "imageCache", /* @__PURE__ */ new Map());
__publicField(this, "iFrameRefs", /* @__PURE__ */ new Map());
/**
* Indicates whether the embeddable's url has been validated for rendering.
* If value not set, indicates that the validation is pending.
* Initially or on url change the flag is not reset so that we can guarantee
* the validation came from a trusted source (the editor).
**/
__publicField(this, "embedsValidationStatus", /* @__PURE__ */ new Map());
/** embeds that have been inserted to DOM (as a perf optim, we don't want to
* insert to DOM before user initially scrolls to them) */
__publicField(this, "initializedEmbeds", /* @__PURE__ */ new Set());
__publicField(this, "elementsPendingErasure", /* @__PURE__ */ new Set());
__publicField(this, "flowChartCreator", new FlowChartCreator());
__publicField(this, "flowChartNavigator", new FlowChartNavigator());
__publicField(this, "hitLinkElement");
__publicField(this, "lastPointerDownEvent", null);
__publicField(this, "lastPointerUpEvent", null);
__publicField(this, "lastPointerMoveEvent", null);
__publicField(this, "lastPointerMoveCoords", null);
__publicField(this, "lastViewportPosition", { x: 0, y: 0 });
__publicField(this, "animationFrameHandler", new AnimationFrameHandler());
__publicField(this, "laserTrails", new LaserTrails(this.animationFrameHandler, this));
__publicField(this, "eraserTrail", new AnimatedTrail(this.animationFrameHandler, this, {
streamline: 0.2,
size: 5,
keepHead: true,
sizeMapping: (c) => {
const DECAY_TIME = 200;
const DECAY_LENGTH = 10;
const t2 = Math.max(0, 1 - (performance.now() - c.pressure) / DECAY_TIME);
const l = (DECAY_LENGTH - Math.min(DECAY_LENGTH, c.totalLength - c.currentIndex)) / DECAY_LENGTH;
return Math.min(easeOut(l), easeOut(t2));
},
fill: () => this.state.theme === THEME.LIGHT ? "rgba(0, 0, 0, 0.2)" : "rgba(255, 255, 255, 0.2)"
}));
__publicField(this, "onChangeEmitter", new Emitter());
__publicField(this, "onPointerDownEmitter", new Emitter());
__publicField(this, "onPointerUpEmitter", new Emitter());
__publicField(this, "onUserFollowEmitter", new Emitter());
__publicField(this, "onScrollChangeEmitter", new Emitter());
__publicField(this, "missingPointerEventCleanupEmitter", new Emitter());
__publicField(this, "onRemoveEventListenersEmitter", new Emitter());
/**
* Returns gridSize taking into account `gridModeEnabled`.
* If disabled, returns null.
*/
__publicField(this, "getEffectiveGridSize", () => {
return isGridModeEnabled(this) ? this.state.gridSize : null;
});
__publicField(this, "updateEmbedValidationStatus", (element, status) => {
this.embedsValidationStatus.set(element.id, status);
ShapeCache.delete(element);
});
__publicField(this, "updateEmbeddables", () => {
const iframeLikes = /* @__PURE__ */ new Set();
let updated = false;
this.scene.getNonDeletedElements().filter((element) => {
if (isEmbeddableElement(element)) {
iframeLikes.add(element.id);
if (!this.embedsValidationStatus.has(element.id)) {
updated = true;
const validated = embeddableURLValidator(
element.link,
this.props.validateEmbeddable
);
this.updateEmbedValidationStatus(element, validated);
}
} else if (isIframeElement(element)) {
iframeLikes.add(element.id);
}
return false;
});
if (updated) {
this.scene.triggerUpdate();
}
this.iFrameRefs.forEach((ref, id) => {
if (!iframeLikes.has(id)) {
this.iFrameRefs.delete(id);
}
});
});
__publicField(this, "getFrameNameDOMId", (frameElement) => {
return `${this.id}-frame-name-${frameElement.id}`;
});
__publicField(this, "frameNameBoundsCache", {
get: (frameElement) => {
let bounds = this.frameNameBoundsCache._cache.get(frameElement.id);
if (!bounds || bounds.zoom !== this.state.zoom.value || bounds.versionNonce !== frameElement.versionNonce) {
const frameNameDiv = document.getElementById(
this.getFrameNameDOMId(frameElement)
);
if (frameNameDiv) {
const box = frameNameDiv.getBoundingClientRect();
const boxSceneTopLeft = viewportCoordsToSceneCoords(
{ clientX: box.x, clientY: box.y },
this.state
);
const boxSceneBottomRight = viewportCoordsToSceneCoords(
{ clientX: box.right, clientY: box.bottom },
this.state
);
bounds = {
x: boxSceneTopLeft.x,
y: boxSceneTopLeft.y,
width: boxSceneBottomRight.x - boxSceneTopLeft.x,
height: boxSceneBottomRight.y - boxSceneTopLeft.y,
angle: 0,
zoom: this.state.zoom.value,
versionNonce: frameElement.versionNonce
};
this.frameNameBoundsCache._cache.set(frameElement.id, bounds);
return bounds;
}
return null;
}
return bounds;
},
/**
* @private
*/
_cache: /* @__PURE__ */ new Map()
});
__publicField(this, "resetEditingFrame", (frame) => {
if (frame) {
mutateElement(frame, { name: frame.name?.trim() || null });
}
this.setState({ editingFrame: null });
});
__publicField(this, "renderFrameNames", () => {
if (!this.state.frameRendering.enabled || !this.state.frameRendering.name) {
if (this.state.editingFrame) {
this.resetEditingFrame(null);
}
return null;
}
const isDarkTheme = this.state.theme === THEME.DARK;
return this.scene.getNonDeletedFramesLikes().map((f) => {
if (!isElementInViewport(
f,
this.canvas.width / window.devicePixelRatio,
this.canvas.height / window.devicePixelRatio,
{
offsetLeft: this.state.offsetLeft,
offsetTop: this.state.offsetTop,
scrollX: this.state.scrollX,
scrollY: this.state.scrollY,
zoom: this.state.zoom
},
this.scene.getNonDeletedElementsMap()
)) {
if (this.state.editingFrame === f.id) {
this.resetEditingFrame(f);
}
return null;
}
const { x: x1, y: y1 } = sceneCoordsToViewportCoords(
{ sceneX: f.x, sceneY: f.y },
this.state
);
const FRAME_NAME_EDIT_PADDING = 6;
let frameNameJSX;
const frameName = getFrameLikeTitle(f);
if (f.id === this.state.editingFrame) {
const frameNameInEdit = frameName;
frameNameJSX = /* @__PURE__ */ jsx147(
"input",
{
autoFocus: true,
value: frameNameInEdit,
onChange: (e) => {
mutateElement(f, {
name: e.target.value
});
},
onFocus: (e) => e.target.select(),
onBlur: () => this.resetEditingFrame(f),
onKeyDown: (event) => {
if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
this.resetEditingFrame(f);
}
},
style: {
background: this.state.viewBackgroundColor,
filter: isDarkTheme ? THEME_FILTER : "none",
zIndex: 2,
border: "none",
display: "block",
padding: `${FRAME_NAME_EDIT_PADDING}px`,
borderRadius: 4,
boxShadow: "inset 0 0 0 1px var(--color-primary)",
fontFamily: "Assistant",
fontSize: "14px",
transform: `translate(-${FRAME_NAME_EDIT_PADDING}px, ${FRAME_NAME_EDIT_PADDING}px)`,
color: "var(--color-gray-80)",
overflow: "hidden",
maxWidth: `${document.body.clientWidth - x1 - FRAME_NAME_EDIT_PADDING}px`
},
size: frameNameInEdit.length + 1 || 1,
dir: "auto",
autoComplete: "off",
autoCapitalize: "off",
autoCorrect: "off"
}
);
} else {
frameNameJSX = frameName;
}
return /* @__PURE__ */ jsx147(
"div",
{
id: this.getFrameNameDOMId(f),
style: {
position: "absolute",
// Positioning from bottom so that we don't to either
// calculate text height or adjust using transform (which)
// messes up input position when editing the frame name.
// This makes the positioning deterministic and we can calculate
// the same position when rendering to canvas / svg.
bottom: `${this.state.height + FRAME_STYLE.nameOffsetY - y1 + this.state.offsetTop}px`,
left: `${x1 - this.state.offsetLeft}px`,
zIndex: 2,
fontSize: FRAME_STYLE.nameFontSize,
color: isDarkTheme ? FRAME_STYLE.nameColorDarkTheme : FRAME_STYLE.nameColorLightTheme,
lineHeight: FRAME_STYLE.nameLineHeight,
width: "max-content",
maxWidth: `${f.width}px`,
overflow: f.id === this.state.editingFrame ? "visible" : "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
cursor: CURSOR_TYPE.MOVE,
pointerEvents: this.state.viewModeEnabled ? POINTER_EVENTS.disabled : POINTER_EVENTS.enabled
},
onPointerDown: (event) => this.handleCanvasPointerDown(event),
onWheel: (event) => this.handleWheel(event),
onContextMenu: this.handleCanvasContextMenu,
onDoubleClick: () => {
this.setState({
editingFrame: f.id
});
},
children: frameNameJSX
},
f.id
);
});
});
__publicField(this, "focusContainer", () => {
this.excalidrawContainerRef.current?.focus();
});
__publicField(this, "getSceneElementsIncludingDeleted", () => {
return this.scene.getElementsIncludingDeleted();
});
__publicField(this, "getSceneElements", () => {
return this.scene.getNonDeletedElements();
});
__publicField(this, "onInsertElements", (elements) => {
this.addElementsFromPasteOrLibrary({
elements,
position: "center",
files: null
});
});
__publicField(this, "onExportImage", async (type, elements, opts) => {
trackEvent("export", type, "ui");
const fileHandle = await exportCanvas(
type,
elements,
this.state,
this.files,
{
exportBackground: this.state.exportBackground,
name: this.getName(),
viewBackgroundColor: this.state.viewBackgroundColor,
exportingFrame: opts.exportingFrame
}
).catch(muteFSAbortError).catch((error) => {
console.error(error);
this.setState({ errorMessage: error.message });
});
if (this.state.exportEmbedScene && fileHandle && isImageFileHandle(fileHandle)) {
this.setState({ fileHandle });
}
});
__publicField(this, "magicGenerations", /* @__PURE__ */ new Map());
__publicField(this, "updateMagicGeneration", ({
frameElement,
data
}) => {
if (data.status === "pending") {
mutateElement(
frameElement,
{ customData: { generationData: void 0 } },
false
);
} else {
mutateElement(
frameElement,
{ customData: { generationData: data } },
false
);
}
this.magicGenerations.set(frameElement.id, data);
this.triggerRender();
});
__publicField(this, "plugins", {});
__publicField(this, "onMagicframeToolSelect", () => {
const selectedElements = this.scene.getSelectedElements({
selectedElementIds: this.state.selectedElementIds
});
if (selectedElements.length === 0) {
this.setActiveTool({ type: TOOL_TYPE.magicframe });
trackEvent("ai", "tool-select (empty-selection)", "d2c");
} else {
const selectedMagicFrame = selectedElements.length === 1 && isMagicFrameElement(selectedElements[0]) && selectedElements[0];
if (!selectedMagicFrame && selectedElements.some((el) => isFrameLikeElement(el) || el.frameId)) {
this.setActiveTool({ type: TOOL_TYPE.magicframe });
return;
}
trackEvent("ai", "tool-select (existing selection)", "d2c");
let frame;
if (selectedMagicFrame) {
frame = selectedMagicFrame;
} else {
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
const padding = 50;
frame = newMagicFrameElement({
...FRAME_STYLE,
x: minX - padding,
y: minY - padding,
width: maxX - minX + padding * 2,
height: maxY - minY + padding * 2,
opacity: 100,
locked: false
});
this.scene.insertElement(frame);
for (const child of selectedElements) {
mutateElement(child, { frameId: frame.id });
}
this.setState({
selectedElementIds: { [frame.id]: true }
});
}
this.onMagicFrameGenerate(frame, "upstream");
}
});
__publicField(this, "openEyeDropper", ({ type }) => {
editorJotaiStore.set(activeEyeDropperAtom, {
swapPreviewOnAlt: true,
colorPickerType: type === "stroke" ? "elementStroke" : "elementBackground",
onSelect: (color, event) => {
const shouldUpdateStrokeColor = type === "background" && event.altKey || type === "stroke" && !event.altKey;
const selectedElements = this.scene.getSelectedElements(this.state);
if (!selectedElements.length || this.state.activeTool.type !== "selection") {
if (shouldUpdateStrokeColor) {
this.syncActionResult({
appState: { ...this.state, currentItemStrokeColor: color },
captureUpdate: CaptureUpdateAction.IMMEDIATELY
});
} else {
this.syncActionResult({
appState: { ...this.state, currentItemBackgroundColor: color },
captureUpdate: CaptureUpdateAction.IMMEDIATELY
});
}
} else {
this.updateScene({
elements: this.scene.getElementsIncludingDeleted().map((el) => {
if (this.state.selectedElementIds[el.id]) {
return newElementWith(el, {
[shouldUpdateStrokeColor ? "strokeColor" : "backgroundColor"]: color
});
}
return el;
}),
captureUpdate: CaptureUpdateAction.IMMEDIATELY
});
}
},
keepOpenOnAlt: false
});
});
__publicField(this, "dismissLinearEditor", () => {
setTimeout(() => {
this.setState({
editingLinearElement: null
});
});
});
__publicField(this, "syncActionResult", withBatchedUpdates((actionResult) => {
if (this.unmounted || actionResult === false) {
return;
}
if (actionResult.captureUpdate === CaptureUpdateAction.NEVER) {
this.store.shouldUpdateSnapshot();
} else if (actionResult.captureUpdate === CaptureUpdateAction.IMMEDIATELY) {
this.store.shouldCaptureIncrement();
}
let didUpdate = false;
let editingTextElement = null;
if (actionResult.elements) {
this.scene.replaceAllElements(actionResult.elements);
didUpdate = true;
}
if (actionResult.files) {
this.addMissingFiles(actionResult.files, actionResult.replaceFiles);
this.addNewImagesToImageCache();
}
if (actionResult.appState || editingTextElement || this.state.contextMenu) {
let viewModeEnabled = actionResult?.appState?.viewModeEnabled || false;
let zenModeEnabled = actionResult?.appState?.zenModeEnabled || false;
const theme = actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
const name = actionResult?.appState?.name ?? this.state.name;
const errorMessage = actionResult?.appState?.errorMessage ?? this.state.errorMessage;
if (typeof this.props.viewModeEnabled !== "undefined") {
viewModeEnabled = this.props.viewModeEnabled;
}
if (typeof this.props.zenModeEnabled !== "undefined") {
zenModeEnabled = this.props.zenModeEnabled;
}
editingTextElement = actionResult.appState?.editingTextElement || null;
if (actionResult.elements && editingTextElement) {
actionResult.elements.forEach((element) => {
if (editingTextElement?.id === element.id && editingTextElement !== element && isNonDeletedElement(element) && isTextElement(element)) {
editingTextElement = element;
}
});
}
if (editingTextElement?.isDeleted) {
editingTextElement = null;
}
this.setState((prevAppState) => {
const actionAppState = actionResult.appState || {};
return {
...prevAppState,
...actionAppState,
// NOTE this will prevent opening context menu using an action
// or programmatically from the host, so it will need to be
// rewritten later
contextMenu: null,
editingTextElement,
viewModeEnabled,
zenModeEnabled,
theme,
name,
errorMessage
};
});
didUpdate = true;
}
if (!didUpdate && actionResult.captureUpdate !== CaptureUpdateAction.EVENTUALLY) {
this.scene.triggerUpdate();
}
}));
// Lifecycle
__publicField(this, "onBlur", withBatchedUpdates(() => {
isHoldingSpace = false;
this.setState({ isBindingEnabled: true });
}));
__publicField(this, "onUnload", () => {
this.onBlur();
});
__publicField(this, "disableEvent", (event) => {
event.preventDefault();
});
__publicField(this, "resetHistory", () => {
this.history.clear();
});
__publicField(this, "resetStore", () => {
this.store.clear();
});
/**
* Resets scene & history.
* ! Do not use to clear scene user action !
*/
__publicField(this, "resetScene", withBatchedUpdates(
(opts) => {
this.scene.replaceAllElements([]);
this.setState((state) => ({
...getDefaultAppState(),
isLoading: opts?.resetLoadingState ? false : state.isLoading,
theme: this.state.theme
}));
this.resetStore();
this.resetHistory();
}
));
__publicField(this, "initializeScene", async () => {
if ("launchQueue" in window && "LaunchParams" in window) {
window.launchQueue.setConsumer(
async (launchParams) => {
if (!launchParams.files.length) {
return;
}
const fileHandle = launchParams.files[0];
const blob = await fileHandle.getFile();
this.loadFileToCanvas(
new File([blob], blob.name || "", { type: blob.type }),
fileHandle
);
}
);
}
if (this.props.theme) {
this.setState({ theme: this.props.theme });
}
if (!this.state.isLoading) {
this.setState({ isLoading: true });
}
let initialData = null;
try {
if (typeof this.props.initialData === "function") {
initialData = await this.props.initialData() || null;
} else {
initialData = await this.props.initialData || null;
}
if (initialData?.libraryItems) {
this.library.updateLibrary({
libraryItems: initialData.libraryItems,
merge: true
}).catch((error) => {
console.error(error);
});
}
} catch (error) {
console.error(error);
initialData = {
appState: {
errorMessage: error.message || "Encountered an error during importing or restoring scene data"
}
};
}
const scene = restore(initialData, null, null, { repairBindings: true });
scene.appState = {
...scene.appState,
theme: this.props.theme || scene.appState.theme,
// we're falling back to current (pre-init) state when deciding
// whether to open the library, to handle a case where we
// update the state outside of initialData (e.g. when loading the app
// with a library install link, which should auto-open the library)
openSidebar: scene.appState?.openSidebar || this.state.openSidebar,
activeTool: scene.appState.activeTool.type === "image" ? { ...scene.appState.activeTool, type: "selection" } : scene.appState.activeTool,
isLoading: false,
toast: this.state.toast
};
if (initialData?.scrollToContent) {
scene.appState = {
...scene.appState,
...calculateScrollCenter(scene.elements, {
...scene.appState,
width: this.state.width,
height: this.state.height,
offsetTop: this.state.offsetTop,
offsetLeft: this.state.offsetLeft
})
};
}
this.resetStore();
this.resetHistory();
this.syncActionResult({
...scene,
captureUpdate: CaptureUpdateAction.NEVER
});
this.clearImageShapeCache();
this.fonts.loadSceneFonts().then((fontFaces) => {
this.fonts.onLoaded(fontFaces);
});
if (isElementLink(window.location.href)) {
this.scrollToContent(window.location.href, { animate: false });
}
});
__publicField(this, "isMobileBreakpoint", (width, height) => {
return width < MQ_MAX_WIDTH_PORTRAIT || height < MQ_MAX_HEIGHT_LANDSCAPE && width < MQ_MAX_WIDTH_LANDSCAPE;
});
__publicField(this, "refreshViewportBreakpoints", () => {
const container = this.excalidrawContainerRef.current;
if (!container) {
return;
}
const { clientWidth: viewportWidth, clientHeight: viewportHeight } = document.body;
const prevViewportState = this.device.viewport;
const nextViewportState = updateObject(prevViewportState, {
isLandscape: viewportWidth > viewportHeight,
isMobile: this.isMobileBreakpoint(viewportWidth, viewportHeight)
});
if (prevViewportState !== nextViewportState) {
this.device = { ...this.device, viewport: nextViewportState };
return true;
}
return false;
});
__publicField(this, "refreshEditorBreakpoints", () => {
const container = this.excalidrawContainerRef.current;
if (!container) {
return;
}
const { width: editorWidth, height: editorHeight } = container.getBoundingClientRect();
const sidebarBreakpoint = this.props.UIOptions.dockedSidebarBreakpoint != null ? this.props.UIOptions.dockedSidebarBreakpoint : MQ_RIGHT_SIDEBAR_MIN_WIDTH;
const prevEditorState = this.device.editor;
const nextEditorState = updateObject(prevEditorState, {
isMobile: this.isMobileBreakpoint(editorWidth, editorHeight),
canFitSidebar: editorWidth > sidebarBreakpoint
});
if (prevEditorState !== nextEditorState) {
this.device = { ...this.device, editor: nextEditorState };
return true;
}
return false;
});
__publicField(this, "onResize", withBatchedUpdates(() => {
this.scene.getElementsIncludingDeleted().forEach((element) => ShapeCache.delete(element));
this.refreshViewportBreakpoints();
this.updateDOMRect();
if (!supportsResizeObserver) {
this.refreshEditorBreakpoints();
}
this.setState({});
}));
/** generally invoked only if fullscreen was invoked programmatically */
__publicField(this, "onFullscreenChange", () => {
if (
// points to the iframe element we fullscreened
!document.fullscreenElement && this.state.activeEmbeddable?.state === "active"
) {
this.setState({
activeEmbeddable: null
});
}
});
__publicField(this, "renderInteractiveSceneCallback", ({
atLeastOneVisibleElement,
scrollBars,
elementsMap
}) => {
if (scrollBars) {
currentScrollBars = scrollBars;
}
const scrolledOutside = (
// hide when editing text
this.state.editingTextElement ? false : !atLeastOneVisibleElement && elementsMap.size > 0
);
if (this.state.scrolledOutside !== scrolledOutside) {
this.setState({ scrolledOutside });
}
this.scheduleImageRefresh();
});
__publicField(this, "onScroll", debounce(() => {
const { offsetTop, offsetLeft } = this.getCanvasOffsets();
this.setState((state) => {
if (state.offsetLeft === offsetLeft && state.offsetTop === offsetTop) {
return null;
}
return { offsetTop, offsetLeft };
});
}, SCROLL_TIMEOUT));
// Copy/paste
__publicField(this, "onCut", withBatchedUpdates((event) => {
const isExcalidrawActive = this.excalidrawContainerRef.current?.contains(
document.activeElement
);
if (!isExcalidrawActive || isWritableElement(event.target)) {
return;
}
this.actionManager.executeAction(actionCut, "keyboard", event);
event.preventDefault();
event.stopPropagation();
}));
__publicField(this, "onCopy", withBatchedUpdates((event) => {
const isExcalidrawActive = this.excalidrawContainerRef.current?.contains(
document.activeElement
);
if (!isExcalidrawActive || isWritableElement(event.target)) {
return;
}
this.actionManager.executeAction(actionCopy, "keyboard", event);
event.preventDefault();
event.stopPropagation();
}));
__publicField(this, "onTouchStart", (event) => {
if (isIOS) {
event.preventDefault();
}
if (!didTapTwice) {
didTapTwice = true;
clearTimeout(tappedTwiceTimer);
tappedTwiceTimer = window.setTimeout(
_App.resetTapTwice,
TAP_TWICE_TIMEOUT
);
return;
}
if (didTapTwice && event.touches.length === 1) {
const touch = event.touches[0];
this.handleCanvasDoubleClick({
clientX: touch.clientX,
clientY: touch.clientY
});
didTapTwice = false;
clearTimeout(tappedTwiceTimer);
}
if (event.touches.length === 2) {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
activeEmbeddable: null
});
}
});
__publicField(this, "onTouchEnd", (event) => {
this.resetContextMenuTimer();
if (event.touches.length > 0) {
this.setState({
previousSelectedElementIds: {},
selectedElementIds: makeNextSelectedElementIds(
this.state.previousSelectedElementIds,
this.state
)
});
} else {
gesture.pointers.clear();
}
});
__publicField(this, "pasteFromClipboard", withBatchedUpdates(
async (event) => {
const isPlainPaste = !!IS_PLAIN_PASTE;
const target = document.activeElement;
const isExcalidrawActive = this.excalidrawContainerRef.current?.contains(target);
if (event && !isExcalidrawActive) {
return;
}
const elementUnderCursor = document.elementFromPoint(
this.lastViewportPosition.x,
this.lastViewportPosition.y
);
if (event && (!(elementUnderCursor instanceof HTMLCanvasElement) || isWritableElement(target))) {
return;
}
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
{
clientX: this.lastViewportPosition.x,
clientY: this.lastViewportPosition.y
},
this.state
);
let file = event?.clipboardData?.files[0];
const data = await parseClipboard(event, isPlainPaste);
if (!file && !isPlainPaste) {
if (data.mixedContent) {
return this.addElementsFromMixedContentPaste(data.mixedContent, {
isPlainPaste,
sceneX,
sceneY
});
} else if (data.text) {
const string = data.text.trim();
if (string.startsWith("<svg") && string.endsWith("</svg>")) {
file = SVGStringToFile(string);
}
}
}
if (isSupportedImageFile(file) && !data.spreadsheet) {
if (!this.isToolSupported("image")) {
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
return;
}
const imageElement = this.createImageElement({ sceneX, sceneY });
this.insertImageElement(imageElement, file);
this.initializeImageDimensions(imageElement);
this.setState({
selectedElementIds: makeNextSelectedElementIds(
{
[imageElement.id]: true
},
this.state
)
});
return;
}
if (this.props.onPaste) {
try {
if (await this.props.onPaste(data, event) === false) {
return;
}
} catch (error) {
console.error(error);
}
}
if (data.errorMessage) {
this.setState({ errorMessage: data.errorMessage });
} else if (data.spreadsheet && !isPlainPaste) {
this.setState({
pasteDialog: {
data: data.spreadsheet,
shown: true
}
});
} else if (data.elements) {
const elements = data.programmaticAPI ? convertToExcalidrawElements(
data.elements
) : data.elements;
this.addElementsFromPasteOrLibrary({
elements,
files: data.files || null,
position: "cursor",
retainSeed: isPlainPaste
});
} else if (data.text) {
if (data.text && isMaybeMermaidDefinition(data.text)) {
const api = await import("@excalidraw/mermaid-to-excalidraw");
try {
const { elements: skeletonElements, files } = await api.parseMermaidToExcalidraw(data.text);
const elements = convertToExcalidrawElements(skeletonElements, {
regenerateIds: true
});
this.addElementsFromPasteOrLibrary({
elements,
files,
position: "cursor"
});
return;
} catch (err) {
console.warn(
`parsing pasted text as mermaid definition failed: ${err.message}`
);
}
}
const nonEmptyLines = normalizeEOL(data.text).split(/\n+/).map((s) => s.trim()).filter(Boolean);
const embbeddableUrls = nonEmptyLines.map((str) => maybeParseEmbedSrc(str)).filter((string) => {
return embeddableURLValidator(string, this.props.validateEmbeddable) && (/^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(string) || getEmbedLink(string)?.type === "video");
});
if (!IS_PLAIN_PASTE && embbeddableUrls.length > 0 && // if there were non-embeddable text (lines) mixed in with embeddable
// urls, ignore and paste as text
embbeddableUrls.length === nonEmptyLines.length) {
const embeddables = [];
for (const url of embbeddableUrls) {
const prevEmbeddable = embeddables[embeddables.length - 1];
const embeddable = this.insertEmbeddableElement({
sceneX: prevEmbeddable ? prevEmbeddable.x + prevEmbeddable.width + 20 : sceneX,
sceneY,
link: normalizeLink(url)
});
if (embeddable) {
embeddables.push(embeddable);
}
}
if (embeddables.length) {
this.setState({
selectedElementIds: Object.fromEntries(
embeddables.map((embeddable) => [embeddable.id, true])
)
});
}
return;
}
this.addTextFromPaste(data.text, isPlainPaste);
}
this.setActiveTool({ type: "selection" });
event?.preventDefault();
}
));
__publicField(this, "addElementsFromPasteOrLibrary", (opts) => {
const elements = restoreElements(opts.elements, null, void 0);
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
const elementsCenterX = distance(minX, maxX) / 2;
const elementsCenterY = distance(minY, maxY) / 2;
const clientX = typeof opts.position === "object" ? opts.position.clientX : opts.position === "cursor" ? this.lastViewportPosition.x : this.state.width / 2 + this.state.offsetLeft;
const clientY = typeof opts.position === "object" ? opts.position.clientY : opts.position === "cursor" ? this.lastViewportPosition.y : this.state.height / 2 + this.state.offsetTop;
const { x, y } = viewportCoordsToSceneCoords(
{ clientX, clientY },
this.state
);
const dx = x - elementsCenterX;
const dy = y - elementsCenterY;
const [gridX, gridY] = getGridPoint(dx, dy, this.getEffectiveGridSize());
const newElements = duplicateElements(
elements.map((element) => {
return newElementWith(element, {
x: element.x + gridX - minX,
y: element.y + gridY - minY
});
}),
{
randomizeSeed: !opts.retainSeed
}
);
const prevElements = this.scene.getElementsIncludingDeleted();
let nextElements = [...prevElements, ...newElements];
const mappedNewSceneElements = this.props.onDuplicate?.(
nextElements,
prevElements
);
nextElements = mappedNewSceneElements || nextElements;
syncMovedIndices(nextElements, arrayToMap(newElements));
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
if (topLayerFrame) {
const eligibleElements = filterElementsEligibleAsFrameChildren(
newElements,
topLayerFrame
);
addElementsToFrame(
nextElements,
eligibleElements,
topLayerFrame,
this.state
);
}
this.scene.replaceAllElements(nextElements);
newElements.forEach((newElement2) => {
if (isTextElement(newElement2) && isBoundToContainer(newElement2)) {
const container = getContainerElement(
newElement2,
this.scene.getElementsMapIncludingDeleted()
);
redrawTextBoundingBox(
newElement2,
container,
this.scene.getElementsMapIncludingDeleted()
);
}
});
if (isSafari) {
Fonts.loadElementsFonts(newElements).then((fontFaces) => {
this.fonts.onLoaded(fontFaces);
});
}
if (opts.files) {
this.addMissingFiles(opts.files);
}
this.store.shouldCaptureIncrement();
const nextElementsToSelect = excludeElementsInFramesFromSelection(newElements);
this.setState(
{
...this.state,
// keep sidebar (presumably the library) open if it's docked and
// can fit.
//
// Note, we should close the sidebar only if we're dropping items
// from library, not when pasting from clipboard. Alas.
openSidebar: this.state.openSidebar && this.device.editor.canFitSidebar && editorJotaiStore.get(isSidebarDockedAtom) ? this.state.openSidebar : null,
...selectGroupsForSelectedElements(
{
editingGroupId: null,
selectedElementIds: nextElementsToSelect.reduce(
(acc, element) => {
if (!isBoundToContainer(element)) {
acc[element.id] = true;
}
return acc;
},
{}
)
},
this.scene.getNonDeletedElements(),
this.state,
this
)
},
() => {
if (opts.files) {
this.addNewImagesToImageCache();
}
}
);
this.setActiveTool({ type: "selection" });
if (opts.fitToContent) {
this.scrollToContent(newElements, {
fitToContent: true,
canvasOffsets: this.getEditorUIOffsets()
});
}
});
__publicField(this, "setAppState", (state, callback) => {
this.setState(state, callback);
});
__publicField(this, "removePointer", (event) => {
if (touchTimeout) {
this.resetContextMenuTimer();
}
gesture.pointers.delete(event.pointerId);
});
__publicField(this, "toggleLock", (source = "ui") => {
if (!this.state.activeTool.locked) {
trackEvent(
"toolbar",
"toggleLock",
`${source} (${this.device.editor.isMobile ? "mobile" : "desktop"})`
);
}
this.setState((prevState) => {
return {
activeTool: {
...prevState.activeTool,
...updateActiveTool(
this.state,
prevState.activeTool.locked ? { type: "selection" } : prevState.activeTool
),
locked: !prevState.activeTool.locked
}
};
});
});
__publicField(this, "updateFrameRendering", (opts) => {
this.setState((prevState) => {
const next = typeof opts === "function" ? opts(prevState.frameRendering) : opts;
return {
frameRendering: {
enabled: next?.enabled ?? prevState.frameRendering.enabled,
clip: next?.clip ?? prevState.frameRendering.clip,
name: next?.name ?? prevState.frameRendering.name,
outline: next?.outline ?? prevState.frameRendering.outline
}
};
});
});
__publicField(this, "togglePenMode", (force) => {
this.setState((prevState) => {
return {
penMode: force ?? !prevState.penMode,
penDetected: true
};
});
});
__publicField(this, "onHandToolToggle", () => {
this.actionManager.executeAction(actionToggleHandTool);
});
/**
* Zooms on canvas viewport center
*/
__publicField(this, "zoomCanvas", (value) => {
this.setState({
...getStateForZoom(
{
viewportX: this.state.width / 2 + this.state.offsetLeft,
viewportY: this.state.height / 2 + this.state.offsetTop,
nextZoom: getNormalizedZoom(value)
},
this.state
)
});
});
__publicField(this, "cancelInProgressAnimation", null);
__publicField(this, "scrollToContent", (target = this.scene.getNonDeletedElements(), opts) => {
if (typeof target === "string") {
let id;
if (isElementLink(target)) {
id = parseElementLinkFromURL(target);
} else {
id = target;
}
if (id) {
const elements = this.scene.getElementsFromId(id);
if (elements?.length) {
this.scrollToContent(elements, {
fitToContent: opts?.fitToContent ?? true,
animate: opts?.animate ?? true
});
} else if (isElementLink(target)) {
this.setState({
toast: {
message: t("elementLink.notFound"),
duration: 3e3,
closable: true
}
});
}
}
return;
}
this.cancelInProgressAnimation?.();
const targetElements = Array.isArray(target) ? target : [target];
let zoom = this.state.zoom;
let scrollX = this.state.scrollX;
let scrollY = this.state.scrollY;
if (opts?.fitToContent || opts?.fitToViewport) {
const { appState } = zoomToFit({
canvasOffsets: opts.canvasOffsets,
targetElements,
appState: this.state,
fitToViewport: !!opts?.fitToViewport,
viewportZoomFactor: opts?.viewportZoomFactor,
minZoom: opts?.minZoom,
maxZoom: opts?.maxZoom
});
zoom = appState.zoom;
scrollX = appState.scrollX;
scrollY = appState.scrollY;
} else {
const scroll = calculateScrollCenter(targetElements, this.state);
scrollX = scroll.scrollX;
scrollY = scroll.scrollY;
}
if (opts?.animate) {
const origScrollX = this.state.scrollX;
const origScrollY = this.state.scrollY;
const origZoom = this.state.zoom.value;
const cancel = easeToValuesRAF({
fromValues: {
scrollX: origScrollX,
scrollY: origScrollY,
zoom: origZoom
},
toValues: { scrollX, scrollY, zoom: zoom.value },
interpolateValue: (from, to, progress, key) => {
if (key === "zoom") {
return from * Math.pow(to / from, easeOut(progress));
}
return void 0;
},
onStep: ({ scrollX: scrollX2, scrollY: scrollY2, zoom: zoom2 }) => {
this.setState({
scrollX: scrollX2,
scrollY: scrollY2,
zoom: { value: zoom2 }
});
},
onStart: () => {
this.setState({ shouldCacheIgnoreZoom: true });
},
onEnd: () => {
this.setState({ shouldCacheIgnoreZoom: false });
},
onCancel: () => {
this.setState({ shouldCacheIgnoreZoom: false });
},
duration: opts?.duration ?? 500
});
this.cancelInProgressAnimation = () => {
cancel();
this.cancelInProgressAnimation = null;
};
} else {
this.setState({ scrollX, scrollY, zoom });
}
});
__publicField(this, "maybeUnfollowRemoteUser", () => {
if (this.state.userToFollow) {
this.setState({ userToFollow: null });
}
});
/** use when changing scrollX/scrollY/zoom based on user interaction */
__publicField(this, "translateCanvas", (state) => {
this.cancelInProgressAnimation?.();
this.maybeUnfollowRemoteUser();
this.setState(state);
});
__publicField(this, "setToast", (toast) => {
this.setState({ toast });
});
__publicField(this, "restoreFileFromShare", async () => {
try {
const webShareTargetCache = await caches.open("web-share-target");
const response = await webShareTargetCache.match("shared-file");
if (response) {
const blob = await response.blob();
const file = new File([blob], blob.name || "", { type: blob.type });
this.loadFileToCanvas(file, null);
await webShareTargetCache.delete("shared-file");
window.history.replaceState(null, APP_NAME, window.location.pathname);
}
} catch (error) {
this.setState({ errorMessage: error.message });
}
});
/**
* adds supplied files to existing files in the appState.
* NOTE if file already exists in editor state, the file data is not updated
* */
__publicField(this, "addFiles", withBatchedUpdates(
(files) => {
const { addedFiles } = this.addMissingFiles(files);
this.clearImageShapeCache(addedFiles);
this.scene.triggerUpdate();
this.addNewImagesToImageCache();
}
));
__publicField(this, "addMissingFiles", (files, replace = false) => {
const nextFiles = replace ? {} : { ...this.files };
const addedFiles = {};
const _files = Array.isArray(files) ? files : Object.values(files);
for (const fileData of _files) {
if (nextFiles[fileData.id]) {
continue;
}
addedFiles[fileData.id] = fileData;
nextFiles[fileData.id] = fileData;
if (fileData.mimeType === MIME_TYPES.svg) {
try {
const restoredDataURL = getDataURL_sync(
normalizeSVG(dataURLToString(fileData.dataURL)),
MIME_TYPES.svg
);
if (fileData.dataURL !== restoredDataURL) {
fileData.version = (fileData.version ?? 1) + 1;
fileData.dataURL = restoredDataURL;
}
} catch (error) {
console.error(error);
}
}
}
this.files = nextFiles;
return { addedFiles };
});
__publicField(this, "updateScene", withBatchedUpdates(
(sceneData) => {
const nextElements = syncInvalidIndices(sceneData.elements ?? []);
if (sceneData.captureUpdate && sceneData.captureUpdate !== CaptureUpdateAction.EVENTUALLY) {
const prevCommittedAppState = this.store.snapshot.appState;
const prevCommittedElements = this.store.snapshot.elements;
const nextCommittedAppState = sceneData.appState ? Object.assign({}, prevCommittedAppState, sceneData.appState) : prevCommittedAppState;
const nextCommittedElements = sceneData.elements ? this.store.filterUncomittedElements(
this.scene.getElementsMapIncludingDeleted(),
// Only used to detect uncomitted local elements
arrayToMap(nextElements)
// We expect all (already reconciled) elements
) : prevCommittedElements;
if (sceneData.captureUpdate === CaptureUpdateAction.IMMEDIATELY) {
this.store.captureIncrement(
nextCommittedElements,
nextCommittedAppState
);
} else if (sceneData.captureUpdate === CaptureUpdateAction.NEVER) {
this.store.updateSnapshot(
nextCommittedElements,
nextCommittedAppState
);
}
}
if (sceneData.appState) {
this.setState(sceneData.appState);
}
if (sceneData.elements) {
this.scene.replaceAllElements(nextElements);
}
if (sceneData.collaborators) {
this.setState({ collaborators: sceneData.collaborators });
}
}
));
__publicField(this, "triggerRender", (force) => {
if (force === true) {
this.scene.triggerUpdate();
} else {
this.setState({});
}
});
/**
* @returns whether the menu was toggled on or off
*/
__publicField(this, "toggleSidebar", ({
name,
tab,
force
}) => {
let nextName;
if (force === void 0) {
nextName = this.state.openSidebar?.name === name && this.state.openSidebar?.tab === tab ? null : name;
} else {
nextName = force ? name : null;
}
const nextState = nextName ? { name: nextName } : null;
if (nextState && tab) {
nextState.tab = tab;
}
this.setState({ openSidebar: nextState });
return !!nextName;
});
__publicField(this, "updateCurrentCursorPosition", withBatchedUpdates(
(event) => {
this.lastViewportPosition.x = event.clientX;
this.lastViewportPosition.y = event.clientY;
}
));
__publicField(this, "getEditorUIOffsets", () => {
const toolbarBottom = this.excalidrawContainerRef?.current?.querySelector(".App-toolbar")?.getBoundingClientRect()?.bottom ?? 0;
const sidebarRect = this.excalidrawContainerRef?.current?.querySelector(".sidebar")?.getBoundingClientRect();
const propertiesPanelRect = this.excalidrawContainerRef?.current?.querySelector(".App-menu__left")?.getBoundingClientRect();
const PADDING = 16;
return getLanguage().rtl ? {
top: toolbarBottom + PADDING,
right: Math.max(
this.state.width - (propertiesPanelRect?.left ?? this.state.width),
0
) + PADDING,
bottom: PADDING,
left: Math.max(sidebarRect?.right ?? 0, 0) + PADDING
} : {
top: toolbarBottom + PADDING,
right: Math.max(
this.state.width - (sidebarRect?.left ?? this.state.width) + PADDING,
0
),
bottom: PADDING,
left: Math.max(propertiesPanelRect?.right ?? 0, 0) + PADDING
};
});
// Input handling
__publicField(this, "onKeyDown", withBatchedUpdates(
(event) => {
if ("Proxy" in window && (!event.shiftKey && /^[A-Z]$/.test(event.key) || event.shiftKey && /^[a-z]$/.test(event.key))) {
event = new Proxy(event, {
get(ev, prop) {
const value = ev[prop];
if (typeof value === "function") {
return value.bind(ev);
}
return prop === "key" ? (
// CapsLock inverts capitalization based on ShiftKey, so invert
// it back
event.shiftKey ? ev.key.toUpperCase() : ev.key.toLowerCase()
) : value;
}
});
}
if (!isInputLike(event.target)) {
if ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && this.state.croppingElementId) {
this.finishImageCropping();
return;
}
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElementsMap(),
this.state
);
if (selectedElements.length === 1 && isImageElement(selectedElements[0]) && event.key === KEYS.ENTER) {
this.startImageCropping(selectedElements[0]);
return;
}
if (event.key === KEYS.ESCAPE && this.flowChartCreator.isCreatingChart) {
this.flowChartCreator.clear();
this.triggerRender(true);
return;
}
const arrowKeyPressed = isArrowKey(event.key);
if (event[KEYS.CTRL_OR_CMD] && arrowKeyPressed && !event.shiftKey) {
event.preventDefault();
const selectedElements2 = getSelectedElements(
this.scene.getNonDeletedElementsMap(),
this.state
);
if (selectedElements2.length === 1 && isFlowchartNodeElement(selectedElements2[0])) {
this.flowChartCreator.createNodes(
selectedElements2[0],
this.scene.getNonDeletedElementsMap(),
this.state,
getLinkDirectionFromKey(event.key)
);
}
if (this.flowChartCreator.pendingNodes?.length && !isElementCompletelyInViewport(
this.flowChartCreator.pendingNodes,
this.canvas.width / window.devicePixelRatio,
this.canvas.height / window.devicePixelRatio,
{
offsetLeft: this.state.offsetLeft,
offsetTop: this.state.offsetTop,
scrollX: this.state.scrollX,
scrollY: this.state.scrollY,
zoom: this.state.zoom
},
this.scene.getNonDeletedElementsMap(),
this.getEditorUIOffsets()
)) {
this.scrollToContent(this.flowChartCreator.pendingNodes, {
animate: true,
duration: 300,
fitToContent: true,
canvasOffsets: this.getEditorUIOffsets()
});
}
return;
}
if (event.altKey) {
const selectedElements2 = getSelectedElements(
this.scene.getNonDeletedElementsMap(),
this.state
);
if (selectedElements2.length === 1 && arrowKeyPressed) {
event.preventDefault();
const nextId = this.flowChartNavigator.exploreByDirection(
selectedElements2[0],
this.scene.getNonDeletedElementsMap(),
getLinkDirectionFromKey(event.key)
);
if (nextId) {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
[nextId]: true
},
prevState
)
}));
const nextNode = this.scene.getNonDeletedElementsMap().get(nextId);
if (nextNode && !isElementCompletelyInViewport(
[nextNode],
this.canvas.width / window.devicePixelRatio,
this.canvas.height / window.devicePixelRatio,
{
offsetLeft: this.state.offsetLeft,
offsetTop: this.state.offsetTop,
scrollX: this.state.scrollX,
scrollY: this.state.scrollY,
zoom: this.state.zoom
},
this.scene.getNonDeletedElementsMap(),
this.getEditorUIOffsets()
)) {
this.scrollToContent(nextNode, {
animate: true,
duration: 300,
canvasOffsets: this.getEditorUIOffsets()
});
}
}
return;
}
}
}
if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.P && !event.shiftKey && !event.altKey) {
this.setToast({
message: t("commandPalette.shortcutHint", {
shortcut: getShortcutFromShortcutName("commandPalette")
})
});
event.preventDefault();
return;
}
if (event[KEYS.CTRL_OR_CMD] && event.key.toLowerCase() === KEYS.V) {
IS_PLAIN_PASTE = event.shiftKey;
clearTimeout(IS_PLAIN_PASTE_TIMER);
IS_PLAIN_PASTE_TIMER = window.setTimeout(() => {
IS_PLAIN_PASTE = false;
}, 100);
}
if (event[KEYS.CTRL_OR_CMD] && isWritableElement(event.target)) {
if (event.code === CODES.MINUS || event.code === CODES.EQUAL) {
event.preventDefault();
return;
}
}
if (
// inside an input
isWritableElement(event.target) && // unless pressing escape (finalize action)
event.key !== KEYS.ESCAPE || // or unless using arrows (to move between buttons)
isArrowKey(event.key) && isInputLike(event.target)
) {
return;
}
if (event.key === KEYS.QUESTION_MARK) {
this.setState({
openDialog: { name: "help" }
});
return;
} else if (event.key.toLowerCase() === KEYS.E && event.shiftKey && event[KEYS.CTRL_OR_CMD]) {
event.preventDefault();
this.setState({ openDialog: { name: "imageExport" } });
return;
}
if (event.key === KEYS.PAGE_UP || event.key === KEYS.PAGE_DOWN) {
let offset = (event.shiftKey ? this.state.width : this.state.height) / this.state.zoom.value;
if (event.key === KEYS.PAGE_DOWN) {
offset = -offset;
}
if (event.shiftKey) {
this.translateCanvas((state) => ({
scrollX: state.scrollX + offset
}));
} else {
this.translateCanvas((state) => ({
scrollY: state.scrollY + offset
}));
}
}
if (this.state.openDialog?.name === "elementLinkSelector") {
return;
}
if (this.actionManager.handleKeyDown(event)) {
return;
}
if (this.state.viewModeEnabled) {
return;
}
if (event[KEYS.CTRL_OR_CMD] && this.state.isBindingEnabled) {
this.setState({ isBindingEnabled: false });
}
if (isArrowKey(event.key)) {
let selectedElements = this.scene.getSelectedElements({
selectedElementIds: this.state.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true
});
const elbowArrow = selectedElements.find(isElbowArrow);
const arrowIdsToRemove = /* @__PURE__ */ new Set();
selectedElements.filter(isElbowArrow).filter((arrow) => {
const startElementNotInSelection = arrow.startBinding && !selectedElements.some(
(el) => el.id === arrow.startBinding?.elementId
);
const endElementNotInSelection = arrow.endBinding && !selectedElements.some(
(el) => el.id === arrow.endBinding?.elementId
);
return startElementNotInSelection || endElementNotInSelection;
}).forEach((arrow) => arrowIdsToRemove.add(arrow.id));
selectedElements = selectedElements.filter(
(el) => !arrowIdsToRemove.has(el.id)
);
const step = this.getEffectiveGridSize() && (event.shiftKey ? ELEMENT_TRANSLATE_AMOUNT : this.getEffectiveGridSize()) || (event.shiftKey ? ELEMENT_SHIFT_TRANSLATE_AMOUNT : ELEMENT_TRANSLATE_AMOUNT);
let offsetX = 0;
let offsetY = 0;
if (event.key === KEYS.ARROW_LEFT) {
offsetX = -step;
} else if (event.key === KEYS.ARROW_RIGHT) {
offsetX = step;
} else if (event.key === KEYS.ARROW_UP) {
offsetY = -step;
} else if (event.key === KEYS.ARROW_DOWN) {
offsetY = step;
}
selectedElements.forEach((element) => {
mutateElement(
element,
{
x: element.x + offsetX,
y: element.y + offsetY
},
false
);
updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: selectedElements
});
});
this.setState({
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements.filter(
(element) => element.id !== elbowArrow?.id || step !== 0
),
this.scene.getNonDeletedElementsMap(),
this.state.zoom
)
});
this.scene.triggerUpdate();
event.preventDefault();
} else if (event.key === KEYS.ENTER) {
const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1) {
const selectedElement = selectedElements[0];
if (event[KEYS.CTRL_OR_CMD]) {
if (isLinearElement(selectedElement)) {
if (!this.state.editingLinearElement || this.state.editingLinearElement.elementId !== selectedElements[0].id) {
this.store.shouldCaptureIncrement();
if (!isElbowArrow(selectedElement)) {
this.setState({
editingLinearElement: new LinearElementEditor(
selectedElement
)
});
}
}
}
} else if (isTextElement(selectedElement) || isValidTextContainer(selectedElement)) {
let container;
if (!isTextElement(selectedElement)) {
container = selectedElement;
}
const midPoint = getContainerCenter(
selectedElement,
this.state,
this.scene.getNonDeletedElementsMap()
);
const sceneX = midPoint.x;
const sceneY = midPoint.y;
this.startTextEditing({
sceneX,
sceneY,
container
});
event.preventDefault();
return;
} else if (isFrameLikeElement(selectedElement)) {
this.setState({
editingFrame: selectedElement.id
});
}
}
} else if (!event.ctrlKey && !event.altKey && !event.metaKey && !this.state.newElement && !this.state.selectionElement && !this.state.selectedElementsAreBeingDragged) {
const shape = findShapeByKey(event.key);
if (shape) {
if (this.state.activeTool.type !== shape) {
trackEvent(
"toolbar",
shape,
`keyboard (${this.device.editor.isMobile ? "mobile" : "desktop"})`
);
}
if (shape === "arrow" && this.state.activeTool.type === "arrow") {
this.setState((prevState) => ({
currentItemArrowType: prevState.currentItemArrowType === ARROW_TYPE.sharp ? ARROW_TYPE.round : prevState.currentItemArrowType === ARROW_TYPE.round ? ARROW_TYPE.elbow : ARROW_TYPE.sharp
}));
}
this.setActiveTool({ type: shape });
event.stopPropagation();
} else if (event.key === KEYS.Q) {
this.toggleLock("keyboard");
event.stopPropagation();
}
}
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
isHoldingSpace = true;
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
event.preventDefault();
}
if ((event.key === KEYS.G || event.key === KEYS.S) && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
const selectedElements = this.scene.getSelectedElements(this.state);
if (this.state.activeTool.type === "selection" && !selectedElements.length) {
return;
}
if (event.key === KEYS.G && (hasBackground(this.state.activeTool.type) || selectedElements.some((element) => hasBackground(element.type)))) {
this.setState({ openPopup: "elementBackground" });
event.stopPropagation();
}
if (event.key === KEYS.S) {
this.setState({ openPopup: "elementStroke" });
event.stopPropagation();
}
}
if (!event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key.toLowerCase() === KEYS.F) {
const selectedElements = this.scene.getSelectedElements(this.state);
if (this.state.activeTool.type === "selection" && !selectedElements.length) {
return;
}
if (this.state.activeTool.type === "text" || selectedElements.find(
(element) => isTextElement(element) || getBoundTextElement(
element,
this.scene.getNonDeletedElementsMap()
)
)) {
event.preventDefault();
this.setState({ openPopup: "fontFamily" });
}
}
if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
if (this.state.activeTool.type === "laser") {
this.setActiveTool({ type: "selection" });
} else {
this.setActiveTool({ type: "laser" });
}
return;
}
if (event[KEYS.CTRL_OR_CMD] && (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE)) {
editorJotaiStore.set(activeConfirmDialogAtom, "clearCanvas");
}
const lowerCased = event.key.toLocaleLowerCase();
const isPickingStroke = lowerCased === KEYS.S && event.shiftKey;
const isPickingBackground = event.key === KEYS.I || lowerCased === KEYS.G && event.shiftKey;
if (isPickingStroke || isPickingBackground) {
this.openEyeDropper({
type: isPickingStroke ? "stroke" : "background"
});
}
}
));
__publicField(this, "onKeyUp", withBatchedUpdates((event) => {
if (event.key === KEYS.SPACE) {
if (this.state.viewModeEnabled || this.state.openDialog?.name === "elementLinkSelector") {
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
} else if (this.state.activeTool.type === "selection") {
resetCursor(this.interactiveCanvas);
} else {
setCursorForShape(this.interactiveCanvas, this.state);
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedGroupIds: {},
editingGroupId: null,
activeEmbeddable: null
});
}
isHoldingSpace = false;
}
if (!event[KEYS.CTRL_OR_CMD] && !this.state.isBindingEnabled) {
this.setState({ isBindingEnabled: true });
}
if (isArrowKey(event.key)) {
bindOrUnbindLinearElements(
this.scene.getSelectedElements(this.state).filter(isLinearElement),
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.scene,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
this.state.zoom
);
this.setState({ suggestedBindings: [] });
}
if (!event.altKey) {
if (this.flowChartNavigator.isExploring) {
this.flowChartNavigator.clear();
this.syncActionResult({
captureUpdate: CaptureUpdateAction.IMMEDIATELY
});
}
}
if (!event[KEYS.CTRL_OR_CMD]) {
if (this.flowChartCreator.isCreatingChart) {
if (this.flowChartCreator.pendingNodes?.length) {
this.scene.insertElements(this.flowChartCreator.pendingNodes);
}
const firstNode = this.flowChartCreator.pendingNodes?.[0];
if (firstNode) {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
[firstNode.id]: true
},
prevState
)
}));
if (!isElementCompletelyInViewport(
[firstNode],
this.canvas.width / window.devicePixelRatio,
this.canvas.height / window.devicePixelRatio,
{
offsetLeft: this.state.offsetLeft,
offsetTop: this.state.offsetTop,
scrollX: this.state.scrollX,
scrollY: this.state.scrollY,
zoom: this.state.zoom
},
this.scene.getNonDeletedElementsMap(),
this.getEditorUIOffsets()
)) {
this.scrollToContent(firstNode, {
animate: true,
duration: 300,
canvasOffsets: this.getEditorUIOffsets()
});
}
}
this.flowChartCreator.clear();
this.syncActionResult({
captureUpdate: CaptureUpdateAction.IMMEDIATELY
});
}
}
}));
// We purposely widen the `tool` type so this helper can be called with
// any tool without having to type check it
__publicField(this, "isToolSupported", (tool) => {
return this.props.UIOptions.tools?.[tool] !== false;
});
__publicField(this, "setActiveTool", (tool) => {
if (!this.isToolSupported(tool.type)) {
console.warn(
`"${tool.type}" tool is disabled via "UIOptions.canvasActions.tools.${tool.type}"`
);
return;
}
const nextActiveTool = updateActiveTool(this.state, tool);
if (nextActiveTool.type === "hand") {
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
} else if (!isHoldingSpace) {
setCursorForShape(this.interactiveCanvas, {
...this.state,
activeTool: nextActiveTool
});
}
if (isToolIcon(document.activeElement)) {
this.focusContainer();
}
if (!isLinearElementType(nextActiveTool.type)) {
this.setState({ suggestedBindings: [] });
}
if (nextActiveTool.type === "image") {
this.onImageAction({
insertOnCanvasDirectly: (tool.type === "image" && tool.insertOnCanvasDirectly) ?? false
});
}
this.setState((prevState) => {
const commonResets = {
snapLines: prevState.snapLines.length ? [] : prevState.snapLines,
originSnapOffset: null,
activeEmbeddable: null
};
if (nextActiveTool.type === "freedraw") {
this.store.shouldCaptureIncrement();
}
if (nextActiveTool.type !== "selection") {
return {
...prevState,
activeTool: nextActiveTool,
selectedElementIds: makeNextSelectedElementIds({}, prevState),
selectedGroupIds: makeNextSelectedElementIds({}, prevState),
editingGroupId: null,
multiElement: null,
...commonResets
};
}
return {
...prevState,
activeTool: nextActiveTool,
...commonResets
};
});
});
__publicField(this, "setOpenDialog", (dialogType) => {
this.setState({ openDialog: dialogType });
});
__publicField(this, "setCursor", (cursor) => {
setCursor(this.interactiveCanvas, cursor);
});
__publicField(this, "resetCursor", () => {
resetCursor(this.interactiveCanvas);
});
/**
* returns whether user is making a gesture with >= 2 fingers (points)
* on o touch screen (not on a trackpad). Currently only relates to Darwin
* (iOS/iPadOS,MacOS), but may work on other devices in the future if
* GestureEvent is standardized.
*/
__publicField(this, "isTouchScreenMultiTouchGesture", () => {
return gesture.pointers.size >= 2;
});
__publicField(this, "getName", () => {
return this.state.name || this.props.name || `${t("labels.untitled")}-${getDateTime()}`;
});
// fires only on Safari
__publicField(this, "onGestureStart", withBatchedUpdates((event) => {
event.preventDefault();
if (this.isTouchScreenMultiTouchGesture()) {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
activeEmbeddable: null
});
}
gesture.initialScale = this.state.zoom.value;
}));
// fires only on Safari
__publicField(this, "onGestureChange", withBatchedUpdates((event) => {
event.preventDefault();
if (this.isTouchScreenMultiTouchGesture()) {
return;
}
const initialScale = gesture.initialScale;
if (initialScale) {
this.setState((state) => ({
...getStateForZoom(
{
viewportX: this.lastViewportPosition.x,
viewportY: this.lastViewportPosition.y,
nextZoom: getNormalizedZoom(initialScale * event.scale)
},
state
)
}));
}
}));
// fires only on Safari
__publicField(this, "onGestureEnd", withBatchedUpdates((event) => {
event.preventDefault();
if (this.isTouchScreenMultiTouchGesture()) {
this.setState({
previousSelectedElementIds: {},
selectedElementIds: makeNextSelectedElementIds(
this.state.previousSelectedElementIds,
this.state
)
});
}
gesture.initialScale = null;
}));
__publicField(this, "startTextEditing", ({
sceneX,
sceneY,
insertAtParentCenter = true,
container,
autoEdit = true
}) => {
let shouldBindToContainer = false;
let parentCenterPosition = insertAtParentCenter && this.getTextWysiwygSnappedToCenterPosition(
sceneX,
sceneY,
this.state,
container
);
if (container && parentCenterPosition) {
const boundTextElementToContainer = getBoundTextElement(
container,
this.scene.getNonDeletedElementsMap()
);
if (!boundTextElementToContainer) {
shouldBindToContainer = true;
}
}
let existingTextElement = null;
const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1) {
if (isTextElement(selectedElements[0])) {
existingTextElement = selectedElements[0];
} else if (container) {
existingTextElement = getBoundTextElement(
selectedElements[0],
this.scene.getNonDeletedElementsMap()
);
} else {
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
}
} else {
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
}
const fontFamily = existingTextElement?.fontFamily || this.state.currentItemFontFamily;
const lineHeight = existingTextElement?.lineHeight || getLineHeight(fontFamily);
const fontSize = this.state.currentItemFontSize;
if (!existingTextElement && shouldBindToContainer && container && !isArrowElement(container)) {
const fontString = {
fontSize,
fontFamily
};
const minWidth = getApproxMinLineWidth(
getFontString(fontString),
lineHeight
);
const minHeight = getApproxMinLineHeight(fontSize, lineHeight);
const newHeight = Math.max(container.height, minHeight);
const newWidth = Math.max(container.width, minWidth);
mutateElement(container, { height: newHeight, width: newWidth });
sceneX = container.x + newWidth / 2;
sceneY = container.y + newHeight / 2;
if (parentCenterPosition) {
parentCenterPosition = this.getTextWysiwygSnappedToCenterPosition(
sceneX,
sceneY,
this.state,
container
);
}
}
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x: sceneX,
y: sceneY
});
const element = existingTextElement ? existingTextElement : newTextElement({
x: parentCenterPosition ? parentCenterPosition.elementCenterX : sceneX,
y: parentCenterPosition ? parentCenterPosition.elementCenterY : sceneY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
text: "",
fontSize,
fontFamily,
textAlign: parentCenterPosition ? "center" : this.state.currentItemTextAlign,
verticalAlign: parentCenterPosition ? VERTICAL_ALIGN.MIDDLE : DEFAULT_VERTICAL_ALIGN,
containerId: shouldBindToContainer ? container?.id : void 0,
groupIds: container?.groupIds ?? [],
lineHeight,
angle: container?.angle ?? 0,
frameId: topLayerFrame ? topLayerFrame.id : null
});
if (!existingTextElement && shouldBindToContainer && container) {
mutateElement(container, {
boundElements: (container.boundElements || []).concat({
type: "text",
id: element.id
})
});
}
this.setState({ editingTextElement: element });
if (!existingTextElement) {
if (container && shouldBindToContainer) {
const containerIndex = this.scene.getElementIndex(container.id);
this.scene.insertElementAtIndex(element, containerIndex + 1);
} else {
this.scene.insertElement(element);
}
}
if (autoEdit || existingTextElement || container) {
this.handleTextWysiwyg(element, {
isExistingElement: !!existingTextElement
});
} else {
this.setState({
newElement: element,
multiElement: null
});
}
});
__publicField(this, "startImageCropping", (image) => {
this.store.shouldCaptureIncrement();
this.setState({
croppingElementId: image.id
});
});
__publicField(this, "finishImageCropping", () => {
if (this.state.croppingElementId) {
this.store.shouldCaptureIncrement();
this.setState({
croppingElementId: null
});
}
});
__publicField(this, "handleCanvasDoubleClick", (event) => {
if (this.state.multiElement) {
return;
}
if (this.state.activeTool.type !== "selection") {
return;
}
const selectedElements = this.scene.getSelectedElements(this.state);
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
event,
this.state
);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
if (event[KEYS.CTRL_OR_CMD] && (!this.state.editingLinearElement || this.state.editingLinearElement.elementId !== selectedElements[0].id) && !isElbowArrow(selectedElements[0])) {
this.store.shouldCaptureIncrement();
this.setState({
editingLinearElement: new LinearElementEditor(selectedElements[0])
});
return;
} else if (this.state.selectedLinearElement && isElbowArrow(selectedElements[0])) {
const hitCoords = LinearElementEditor.getSegmentMidpointHitCoords(
this.state.selectedLinearElement,
{ x: sceneX, y: sceneY },
this.state,
this.scene.getNonDeletedElementsMap()
);
const midPoint = hitCoords ? LinearElementEditor.getSegmentMidPointIndex(
this.state.selectedLinearElement,
this.state,
hitCoords,
this.scene.getNonDeletedElementsMap()
) : -1;
if (midPoint && midPoint > -1) {
this.store.shouldCaptureIncrement();
LinearElementEditor.deleteFixedSegment(selectedElements[0], midPoint);
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
{
...this.state.selectedLinearElement,
segmentMidPointHoveredCoords: null
},
{ x: sceneX, y: sceneY },
this.state,
this.scene.getNonDeletedElementsMap()
);
const nextIndex = nextCoords ? LinearElementEditor.getSegmentMidPointIndex(
this.state.selectedLinearElement,
this.state,
nextCoords,
this.scene.getNonDeletedElementsMap()
) : null;
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
pointerDownState: {
...this.state.selectedLinearElement.pointerDownState,
segmentMidpoint: {
index: nextIndex,
value: hitCoords,
added: false
}
},
segmentMidPointHoveredCoords: nextCoords
}
});
return;
}
}
}
if (selectedElements.length === 1 && isImageElement(selectedElements[0])) {
this.startImageCropping(selectedElements[0]);
return;
}
resetCursor(this.interactiveCanvas);
const selectedGroupIds = getSelectedGroupIds(this.state);
if (selectedGroupIds.length > 0) {
const hitElement = this.getElementAtPosition(sceneX, sceneY);
const selectedGroupId = hitElement && getSelectedGroupIdForElement(hitElement, this.state.selectedGroupIds);
if (selectedGroupId) {
this.store.shouldCaptureIncrement();
this.setState((prevState) => ({
...prevState,
...selectGroupsForSelectedElements(
{
editingGroupId: selectedGroupId,
selectedElementIds: { [hitElement.id]: true }
},
this.scene.getNonDeletedElements(),
prevState,
this
)
}));
return;
}
}
resetCursor(this.interactiveCanvas);
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
const hitElement = this.getElementAtPosition(sceneX, sceneY);
if (isIframeLikeElement(hitElement)) {
this.setState({
activeEmbeddable: { element: hitElement, state: "active" }
});
return;
}
const container = this.getTextBindableContainerAtPosition(sceneX, sceneY);
if (container) {
if (hasBoundTextElement(container) || !isTransparent(container.backgroundColor) || hitElementItself({
x: sceneX,
y: sceneY,
element: container,
shape: getElementShape(
container,
this.scene.getNonDeletedElementsMap()
),
threshold: this.getElementHitThreshold()
})) {
const midPoint = getContainerCenter(
container,
this.state,
this.scene.getNonDeletedElementsMap()
);
sceneX = midPoint.x;
sceneY = midPoint.y;
}
}
this.startTextEditing({
sceneX,
sceneY,
insertAtParentCenter: !event.altKey,
container
});
}
});
__publicField(this, "getElementLinkAtPosition", (scenePointer, hitElement) => {
const elements = this.scene.getNonDeletedElements();
let hitElementIndex = -1;
for (let index = elements.length - 1; index >= 0; index--) {
const element = elements[index];
if (hitElement && element.id === hitElement.id) {
hitElementIndex = index;
}
if (element.link && index >= hitElementIndex && isPointHittingLink(
element,
this.scene.getNonDeletedElementsMap(),
this.state,
pointFrom(scenePointer.x, scenePointer.y),
this.device.editor.isMobile
)) {
return element;
}
}
});
__publicField(this, "redirectToLink", (event, isTouchScreen) => {
const draggedDistance = pointDistance(
pointFrom(
this.lastPointerDownEvent.clientX,
this.lastPointerDownEvent.clientY
),
pointFrom(
this.lastPointerUpEvent.clientX,
this.lastPointerUpEvent.clientY
)
);
if (!this.hitLinkElement || draggedDistance > DRAGGING_THRESHOLD) {
return;
}
const lastPointerDownCoords = viewportCoordsToSceneCoords(
this.lastPointerDownEvent,
this.state
);
const elementsMap = this.scene.getNonDeletedElementsMap();
const lastPointerDownHittingLinkIcon = isPointHittingLink(
this.hitLinkElement,
elementsMap,
this.state,
pointFrom(lastPointerDownCoords.x, lastPointerDownCoords.y),
this.device.editor.isMobile
);
const lastPointerUpCoords = viewportCoordsToSceneCoords(
this.lastPointerUpEvent,
this.state
);
const lastPointerUpHittingLinkIcon = isPointHittingLink(
this.hitLinkElement,
elementsMap,
this.state,
pointFrom(lastPointerUpCoords.x, lastPointerUpCoords.y),
this.device.editor.isMobile
);
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
hideHyperlinkToolip();
let url = this.hitLinkElement.link;
if (url) {
url = normalizeLink(url);
let customEvent;
if (this.props.onLinkOpen) {
customEvent = wrapEvent("excalidraw-link" /* EXCALIDRAW_LINK */, event.nativeEvent);
this.props.onLinkOpen(
{
...this.hitLinkElement,
link: url
},
customEvent
);
}
if (!customEvent?.defaultPrevented) {
const target = isLocalLink(url) ? "_self" : "_blank";
const newWindow = window.open(void 0, target);
if (newWindow) {
newWindow.opener = null;
newWindow.location = url;
}
}
}
}
});
__publicField(this, "getTopLayerFrameAtSceneCoords", (sceneCoords) => {
const elementsMap = this.scene.getNonDeletedElementsMap();
const frames = this.scene.getNonDeletedFramesLikes().filter(
(frame) => isCursorInFrame(sceneCoords, frame, elementsMap)
);
return frames.length ? frames[frames.length - 1] : null;
});
__publicField(this, "handleCanvasPointerMove", (event) => {
this.savePointer(event.clientX, event.clientY, this.state.cursorButton);
this.lastPointerMoveEvent = event.nativeEvent;
if (gesture.pointers.has(event.pointerId)) {
gesture.pointers.set(event.pointerId, {
x: event.clientX,
y: event.clientY
});
}
const initialScale = gesture.initialScale;
if (gesture.pointers.size === 2 && gesture.lastCenter && initialScale && gesture.initialDistance) {
const center = getCenter(gesture.pointers);
const deltaX = center.x - gesture.lastCenter.x;
const deltaY = center.y - gesture.lastCenter.y;
gesture.lastCenter = center;
const distance2 = getDistance(Array.from(gesture.pointers.values()));
const scaleFactor = this.state.activeTool.type === "freedraw" && this.state.penMode ? 1 : distance2 / gesture.initialDistance;
const nextZoom = scaleFactor ? getNormalizedZoom(initialScale * scaleFactor) : this.state.zoom.value;
this.setState((state) => {
const zoomState = getStateForZoom(
{
viewportX: center.x,
viewportY: center.y,
nextZoom
},
state
);
this.translateCanvas({
zoom: zoomState.zoom,
// 2x multiplier is just a magic number that makes this work correctly
// on touchscreen devices (note: if we get report that panning is slower/faster
// than actual movement, consider swapping with devicePixelRatio)
scrollX: zoomState.scrollX + 2 * (deltaX / nextZoom),
scrollY: zoomState.scrollY + 2 * (deltaY / nextZoom),
shouldCacheIgnoreZoom: true
});
});
this.resetShouldCacheIgnoreZoomDebounced();
} else {
gesture.lastCenter = gesture.initialDistance = gesture.initialScale = null;
}
if (isHoldingSpace || isPanning || isDraggingScrollBar || isHandToolActive(this.state)) {
return;
}
const isPointerOverScrollBars = isOverScrollBars(
currentScrollBars,
event.clientX - this.state.offsetLeft,
event.clientY - this.state.offsetTop
);
const isOverScrollBar = isPointerOverScrollBars.isOverEither;
if (!this.state.newElement && !this.state.selectionElement && !this.state.selectedElementsAreBeingDragged && !this.state.multiElement) {
if (isOverScrollBar) {
resetCursor(this.interactiveCanvas);
} else {
setCursorForShape(this.interactiveCanvas, this.state);
}
}
const scenePointer = viewportCoordsToSceneCoords(event, this.state);
const { x: scenePointerX, y: scenePointerY } = scenePointer;
if (!this.state.newElement && isActiveToolNonLinearSnappable(this.state.activeTool.type)) {
const { originOffset, snapLines } = getSnapLinesAtPointer(
this.scene.getNonDeletedElements(),
this,
{
x: scenePointerX,
y: scenePointerY
},
event,
this.scene.getNonDeletedElementsMap()
);
this.setState((prevState) => {
const nextSnapLines = updateStable(prevState.snapLines, snapLines);
const nextOriginOffset = prevState.originSnapOffset ? updateStable(prevState.originSnapOffset, originOffset) : originOffset;
if (prevState.snapLines === nextSnapLines && prevState.originSnapOffset === nextOriginOffset) {
return null;
}
return {
snapLines: nextSnapLines,
originSnapOffset: nextOriginOffset
};
});
} else if (!this.state.newElement && !this.state.selectedElementsAreBeingDragged && !this.state.selectionElement) {
this.setState((prevState) => {
if (prevState.snapLines.length) {
return {
snapLines: []
};
}
return null;
});
}
if (this.state.editingLinearElement && !this.state.editingLinearElement.isDragging) {
const editingLinearElement = LinearElementEditor.handlePointerMove(
event,
scenePointerX,
scenePointerY,
this,
this.scene.getNonDeletedElementsMap()
);
if (editingLinearElement && editingLinearElement !== this.state.editingLinearElement) {
flushSync2(() => {
this.setState({
editingLinearElement
});
});
}
if (editingLinearElement?.lastUncommittedPoint != null) {
this.maybeSuggestBindingAtCursor(
scenePointer,
editingLinearElement.elbowed
);
} else {
flushSync2(() => {
this.setState({ suggestedBindings: [] });
});
}
}
if (isBindingElementType(this.state.activeTool.type)) {
const { newElement: newElement2 } = this.state;
if (isBindingElement(newElement2, false)) {
this.maybeSuggestBindingsForLinearElementAtCoords(
newElement2,
[scenePointer],
this.state.startBoundElement
);
} else {
this.maybeSuggestBindingAtCursor(scenePointer, false);
}
}
if (this.state.multiElement) {
const { multiElement } = this.state;
const { x: rx, y: ry } = multiElement;
const { points, lastCommittedPoint } = multiElement;
const lastPoint = points[points.length - 1];
setCursorForShape(this.interactiveCanvas, this.state);
if (lastPoint === lastCommittedPoint) {
if (pointDistance(
pointFrom(scenePointerX - rx, scenePointerY - ry),
lastPoint
) >= LINE_CONFIRM_THRESHOLD) {
mutateElement(
multiElement,
{
points: [
...points,
pointFrom(scenePointerX - rx, scenePointerY - ry)
]
},
false
);
} else {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
}
} else if (points.length > 2 && lastCommittedPoint && pointDistance(
pointFrom(scenePointerX - rx, scenePointerY - ry),
lastCommittedPoint
) < LINE_CONFIRM_THRESHOLD) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
mutateElement(
multiElement,
{
points: points.slice(0, -1)
},
false
);
} else {
const [gridX, gridY] = getGridPoint(
scenePointerX,
scenePointerY,
event[KEYS.CTRL_OR_CMD] || isElbowArrow(multiElement) ? null : this.getEffectiveGridSize()
);
const [lastCommittedX, lastCommittedY] = multiElement?.lastCommittedPoint ?? [0, 0];
let dxFromLastCommitted = gridX - rx - lastCommittedX;
let dyFromLastCommitted = gridY - ry - lastCommittedY;
if (shouldRotateWithDiscreteAngle(event)) {
({ width: dxFromLastCommitted, height: dyFromLastCommitted } = getLockedLinearCursorAlignSize(
// actual coordinate of the last committed point
lastCommittedX + rx,
lastCommittedY + ry,
// cursor-grid coordinate
gridX,
gridY
));
}
if (isPathALoop(points, this.state.zoom.value)) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
}
mutateElement(
multiElement,
{
points: [
...points.slice(0, -1),
pointFrom(
lastCommittedX + dxFromLastCommitted,
lastCommittedY + dyFromLastCommitted
)
]
},
false,
{
isDragging: true
}
);
this.triggerRender(false);
}
return;
}
const hasDeselectedButton = Boolean(event.buttons);
if (hasDeselectedButton || this.state.activeTool.type !== "selection" && this.state.activeTool.type !== "text" && this.state.activeTool.type !== "eraser") {
return;
}
const elements = this.scene.getNonDeletedElements();
const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1 && !isOverScrollBar && !this.state.editingLinearElement) {
if (this.state.selectedLinearElement) {
this.handleHoverSelectedLinearElement(
this.state.selectedLinearElement,
scenePointerX,
scenePointerY
);
}
if ((!this.state.selectedLinearElement || this.state.selectedLinearElement.hoverPointIndex === -1) && this.state.openDialog?.name !== "elementLinkSelector" && !(selectedElements.length === 1 && isElbowArrow(selectedElements[0]))) {
const elementWithTransformHandleType = getElementWithTransformHandleType(
elements,
this.state,
scenePointerX,
scenePointerY,
this.state.zoom,
event.pointerType,
this.scene.getNonDeletedElementsMap(),
this.device
);
if (elementWithTransformHandleType && elementWithTransformHandleType.transformHandleType) {
setCursor(
this.interactiveCanvas,
getCursorForResizingElement(elementWithTransformHandleType)
);
return;
}
}
} else if (selectedElements.length > 1 && !isOverScrollBar && this.state.openDialog?.name !== "elementLinkSelector") {
const transformHandleType = getTransformHandleTypeFromCoords(
getCommonBounds(selectedElements),
scenePointerX,
scenePointerY,
this.state.zoom,
event.pointerType,
this.device
);
if (transformHandleType) {
setCursor(
this.interactiveCanvas,
getCursorForResizingElement({
transformHandleType
})
);
return;
}
}
const hitElement = this.getElementAtPosition(
scenePointer.x,
scenePointer.y
);
this.hitLinkElement = this.getElementLinkAtPosition(
scenePointer,
hitElement
);
if (isEraserActive(this.state)) {
return;
}
if (this.hitLinkElement && !this.state.selectedElementIds[this.hitLinkElement.id]) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
showHyperlinkTooltip(
this.hitLinkElement,
this.state,
this.scene.getNonDeletedElementsMap()
);
} else {
hideHyperlinkToolip();
if (hitElement && (hitElement.link || isEmbeddableElement(hitElement)) && this.state.selectedElementIds[hitElement.id] && !this.state.contextMenu && !this.state.showHyperlinkPopup) {
this.setState({ showHyperlinkPopup: "info" });
} else if (this.state.activeTool.type === "text") {
setCursor(
this.interactiveCanvas,
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR
);
} else if (this.state.viewModeEnabled) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
} else if (this.state.openDialog?.name === "elementLinkSelector") {
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
} else if (isOverScrollBar) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
} else if (this.state.selectedLinearElement) {
this.handleHoverSelectedLinearElement(
this.state.selectedLinearElement,
scenePointerX,
scenePointerY
);
} else if (
// if using cmd/ctrl, we're not dragging
!event[KEYS.CTRL_OR_CMD]
) {
if ((hitElement || this.isHittingCommonBoundingBoxOfSelectedElements(
scenePointer,
selectedElements
)) && !hitElement?.locked) {
if (hitElement && isIframeLikeElement(hitElement) && this.isIframeLikeElementCenter(
hitElement,
event,
scenePointerX,
scenePointerY
)) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
this.setState({
activeEmbeddable: { element: hitElement, state: "hover" }
});
} else if (!hitElement || !isElbowArrow(hitElement)) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
if (this.state.activeEmbeddable?.state === "hover") {
this.setState({ activeEmbeddable: null });
}
}
}
} else {
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
}
}
if (this.state.openDialog?.name === "elementLinkSelector" && hitElement) {
this.setState((prevState) => {
return {
hoveredElementIds: updateStable(
prevState.hoveredElementIds,
selectGroupsForSelectedElements(
{
editingGroupId: prevState.editingGroupId,
selectedElementIds: { [hitElement.id]: true }
},
this.scene.getNonDeletedElements(),
prevState,
this
).selectedElementIds
)
};
});
} else if (this.state.openDialog?.name === "elementLinkSelector" && !hitElement) {
this.setState((prevState) => ({
hoveredElementIds: updateStable(prevState.hoveredElementIds, {})
}));
}
});
__publicField(this, "handleEraser", (event, pointerDownState, scenePointer) => {
this.eraserTrail.addPointToPath(scenePointer.x, scenePointer.y);
let didChange = false;
const processedGroups = /* @__PURE__ */ new Set();
const nonDeletedElements = this.scene.getNonDeletedElements();
const processElements = (elements) => {
for (const element of elements) {
if (element.locked) {
return;
}
if (event.altKey) {
if (this.elementsPendingErasure.delete(element.id)) {
didChange = true;
}
} else if (!this.elementsPendingErasure.has(element.id)) {
didChange = true;
this.elementsPendingErasure.add(element.id);
}
if (didChange && element.groupIds?.length) {
const shallowestGroupId = element.groupIds.at(-1);
if (!processedGroups.has(shallowestGroupId)) {
processedGroups.add(shallowestGroupId);
const elems = getElementsInGroup(
nonDeletedElements,
shallowestGroupId
);
for (const elem of elems) {
if (event.altKey) {
this.elementsPendingErasure.delete(elem.id);
} else {
this.elementsPendingErasure.add(elem.id);
}
}
}
}
}
};
const distance2 = pointDistance(
pointFrom(pointerDownState.lastCoords.x, pointerDownState.lastCoords.y),
pointFrom(scenePointer.x, scenePointer.y)
);
const threshold = this.getElementHitThreshold();
const p = { ...pointerDownState.lastCoords };
let samplingInterval = 0;
while (samplingInterval <= distance2) {
const hitElements = this.getElementsAtPosition(p.x, p.y);
processElements(hitElements);
if (samplingInterval === distance2) {
break;
}
samplingInterval = Math.min(samplingInterval + threshold, distance2);
const distanceRatio = samplingInterval / distance2;
const nextX = (1 - distanceRatio) * p.x + distanceRatio * scenePointer.x;
const nextY = (1 - distanceRatio) * p.y + distanceRatio * scenePointer.y;
p.x = nextX;
p.y = nextY;
}
pointerDownState.lastCoords.x = scenePointer.x;
pointerDownState.lastCoords.y = scenePointer.y;
if (didChange) {
for (const element of this.scene.getNonDeletedElements()) {
if (isBoundToContainer(element) && (this.elementsPendingErasure.has(element.id) || this.elementsPendingErasure.has(element.containerId))) {
if (event.altKey) {
this.elementsPendingErasure.delete(element.id);
this.elementsPendingErasure.delete(element.containerId);
} else {
this.elementsPendingErasure.add(element.id);
this.elementsPendingErasure.add(element.containerId);
}
}
}
this.elementsPendingErasure = new Set(this.elementsPendingErasure);
this.triggerRender();
}
});
// set touch moving for mobile context menu
__publicField(this, "handleTouchMove", (event) => {
invalidateContextMenu = true;
});
__publicField(this, "handleCanvasPointerDown", (event) => {
const target = event.target;
if (target.setPointerCapture) {
target.setPointerCapture(event.pointerId);
}
this.maybeCleanupAfterMissingPointerUp(event.nativeEvent);
this.maybeUnfollowRemoteUser();
if (this.state.searchMatches) {
this.setState((state) => ({
searchMatches: state.searchMatches.map((searchMatch) => ({
...searchMatch,
focus: false
}))
}));
editorJotaiStore.set(searchItemInFocusAtom, null);
}
if (this.state.contextMenu) {
this.setState({ contextMenu: null });
}
if (this.state.snapLines) {
this.setAppState({ snapLines: [] });
}
this.updateGestureOnPointerDown(event);
if (event.pointerType === "touch" && this.state.newElement && this.state.newElement.type === "freedraw") {
const element = this.state.newElement;
this.updateScene({
...element.points.length < 10 ? {
elements: this.scene.getElementsIncludingDeleted().filter((el) => el.id !== element.id)
} : {},
appState: {
newElement: null,
editingTextElement: null,
startBoundElement: null,
suggestedBindings: [],
selectedElementIds: makeNextSelectedElementIds(
Object.keys(this.state.selectedElementIds).filter((key) => key !== element.id).reduce((obj, key) => {
obj[key] = this.state.selectedElementIds[key];
return obj;
}, {}),
this.state
)
},
captureUpdate: this.state.openDialog?.name === "elementLinkSelector" ? CaptureUpdateAction.EVENTUALLY : CaptureUpdateAction.NEVER
});
return;
}
const selection = document.getSelection();
if (selection?.anchorNode) {
selection.removeAllRanges();
}
this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event);
if (!this.state.penDetected && event.pointerType === "pen") {
this.setState((prevState) => {
return {
penMode: true,
penDetected: true
};
});
}
if (!this.device.isTouchScreen && ["pen", "touch"].includes(event.pointerType)) {
this.device = updateObject(this.device, { isTouchScreen: true });
}
if (isPanning) {
return;
}
this.lastPointerDownEvent = event;
if (this.handleCanvasPanUsingWheelOrSpaceDrag(event)) {
return;
}
this.setState({
lastPointerDownWith: event.pointerType,
cursorButton: "down"
});
this.savePointer(event.clientX, event.clientY, "down");
if (event.button === POINTER_BUTTON.ERASER && this.state.activeTool.type !== TOOL_TYPE.eraser) {
this.setState(
{
activeTool: updateActiveTool(this.state, {
type: TOOL_TYPE.eraser,
lastActiveToolBeforeEraser: this.state.activeTool
})
},
() => {
this.handleCanvasPointerDown(event);
const onPointerUp2 = () => {
unsubPointerUp();
unsubCleanup?.();
if (isEraserActive(this.state)) {
this.setState({
activeTool: updateActiveTool(this.state, {
...this.state.activeTool.lastActiveTool || {
type: TOOL_TYPE.selection
},
lastActiveToolBeforeEraser: null
})
});
}
};
const unsubPointerUp = addEventListener(
window,
"pointerup" /* POINTER_UP */,
onPointerUp2,
{
once: true
}
);
let unsubCleanup;
requestAnimationFrame(() => {
unsubCleanup = this.missingPointerEventCleanupEmitter.once(onPointerUp2);
});
}
);
return;
}
if (event.button !== POINTER_BUTTON.MAIN && event.button !== POINTER_BUTTON.TOUCH && event.button !== POINTER_BUTTON.ERASER) {
return;
}
if (gesture.pointers.size > 1) {
return;
}
const pointerDownState = this.initialPointerDownState(event);
this.setState({
selectedElementsAreBeingDragged: false
});
if (this.handleDraggingScrollBar(event, pointerDownState)) {
return;
}
this.clearSelectionIfNotUsingSelection();
this.updateBindingEnabledOnPointerMove(event);
if (this.handleSelectionOnPointerDown(event, pointerDownState)) {
return;
}
const allowOnPointerDown = !this.state.penMode || event.pointerType !== "touch" || this.state.activeTool.type === "selection" || this.state.activeTool.type === "text" || this.state.activeTool.type === "image";
if (!allowOnPointerDown) {
return;
}
if (this.state.activeTool.type === "text") {
this.handleTextOnPointerDown(event, pointerDownState);
} else if (this.state.activeTool.type === "arrow" || this.state.activeTool.type === "line") {
this.handleLinearElementOnPointerDown(
event,
this.state.activeTool.type,
pointerDownState
);
} else if (this.state.activeTool.type === "image") {
setCursor(this.interactiveCanvas, CURSOR_TYPE.CROSSHAIR);
const pendingImageElement = this.state.pendingImageElementId && this.scene.getElement(this.state.pendingImageElementId);
if (!pendingImageElement) {
return;
}
this.setState({
newElement: pendingImageElement,
pendingImageElementId: null,
multiElement: null
});
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
const frame = this.getTopLayerFrameAtSceneCoords({ x, y });
mutateElement(pendingImageElement, {
x,
y,
frameId: frame ? frame.id : null
});
} else if (this.state.activeTool.type === "freedraw") {
this.handleFreeDrawElementOnPointerDown(
event,
this.state.activeTool.type,
pointerDownState
);
} else if (this.state.activeTool.type === "custom") {
setCursorForShape(this.interactiveCanvas, this.state);
} else if (this.state.activeTool.type === TOOL_TYPE.frame || this.state.activeTool.type === TOOL_TYPE.magicframe) {
this.createFrameElementOnPointerDown(
pointerDownState,
this.state.activeTool.type
);
} else if (this.state.activeTool.type === "laser") {
this.laserTrails.startPath(
pointerDownState.lastCoords.x,
pointerDownState.lastCoords.y
);
} else if (this.state.activeTool.type !== "eraser" && this.state.activeTool.type !== "hand") {
this.createGenericElementOnPointerDown(
this.state.activeTool.type,
pointerDownState
);
}
this.props?.onPointerDown?.(this.state.activeTool, pointerDownState);
this.onPointerDownEmitter.trigger(
this.state.activeTool,
pointerDownState,
event
);
if (this.state.activeTool.type === "eraser") {
this.eraserTrail.startPath(
pointerDownState.lastCoords.x,
pointerDownState.lastCoords.y
);
}
const onPointerMove = this.onPointerMoveFromPointerDownHandler(pointerDownState);
const onPointerUp = this.onPointerUpFromPointerDownHandler(pointerDownState);
const onKeyDown = this.onKeyDownFromPointerDownHandler(pointerDownState);
const onKeyUp = this.onKeyUpFromPointerDownHandler(pointerDownState);
this.missingPointerEventCleanupEmitter.once(
(_event) => onPointerUp(_event || event.nativeEvent)
);
if (!this.state.viewModeEnabled || this.state.activeTool.type === "laser") {
window.addEventListener("pointermove" /* POINTER_MOVE */, onPointerMove);
window.addEventListener("pointerup" /* POINTER_UP */, onPointerUp);
window.addEventListener("keydown" /* KEYDOWN */, onKeyDown);
window.addEventListener("keyup" /* KEYUP */, onKeyUp);
pointerDownState.eventListeners.onMove = onPointerMove;
pointerDownState.eventListeners.onUp = onPointerUp;
pointerDownState.eventListeners.onKeyUp = onKeyUp;
pointerDownState.eventListeners.onKeyDown = onKeyDown;
}
});
__publicField(this, "handleCanvasPointerUp", (event) => {
this.removePointer(event);
this.lastPointerUpEvent = event;
const scenePointer = viewportCoordsToSceneCoords(
{ clientX: event.clientX, clientY: event.clientY },
this.state
);
const clicklength = event.timeStamp - (this.lastPointerDownEvent?.timeStamp ?? 0);
if (this.device.editor.isMobile && clicklength < 300) {
const hitElement = this.getElementAtPosition(
scenePointer.x,
scenePointer.y
);
if (isIframeLikeElement(hitElement) && this.isIframeLikeElementCenter(
hitElement,
event,
scenePointer.x,
scenePointer.y
)) {
this.handleEmbeddableCenterClick(hitElement);
return;
}
}
if (this.device.isTouchScreen) {
const hitElement = this.getElementAtPosition(
scenePointer.x,
scenePointer.y
);
this.hitLinkElement = this.getElementLinkAtPosition(
scenePointer,
hitElement
);
}
if (this.hitLinkElement && !this.state.selectedElementIds[this.hitLinkElement.id]) {
if (clicklength < 300 && isIframeLikeElement(this.hitLinkElement) && !isPointHittingLinkIcon(
this.hitLinkElement,
this.scene.getNonDeletedElementsMap(),
this.state,
pointFrom(scenePointer.x, scenePointer.y)
)) {
this.handleEmbeddableCenterClick(this.hitLinkElement);
} else {
this.redirectToLink(event, this.device.isTouchScreen);
}
} else if (this.state.viewModeEnabled) {
this.setState({
activeEmbeddable: null,
selectedElementIds: {}
});
}
});
__publicField(this, "maybeOpenContextMenuAfterPointerDownOnTouchDevices", (event) => {
if (event.pointerType === "touch") {
invalidateContextMenu = false;
if (touchTimeout) {
invalidateContextMenu = true;
} else {
touchTimeout = window.setTimeout(() => {
touchTimeout = 0;
if (!invalidateContextMenu) {
this.handleCanvasContextMenu(event);
}
}, TOUCH_CTX_MENU_TIMEOUT);
}
}
});
__publicField(this, "resetContextMenuTimer", () => {
clearTimeout(touchTimeout);
touchTimeout = 0;
invalidateContextMenu = false;
});
/**
* pointerup may not fire in certian cases (user tabs away...), so in order
* to properly cleanup pointerdown state, we need to fire any hanging
* pointerup handlers manually
*/
__publicField(this, "maybeCleanupAfterMissingPointerUp", (event) => {
lastPointerUp?.();
this.missingPointerEventCleanupEmitter.trigger(event).clear();
});
// Returns whether the event is a panning
__publicField(this, "handleCanvasPanUsingWheelOrSpaceDrag", (event) => {
if (!(gesture.pointers.size <= 1 && (event.button === POINTER_BUTTON.WHEEL || event.button === POINTER_BUTTON.MAIN && isHoldingSpace || isHandToolActive(this.state) || this.state.viewModeEnabled))) {
return false;
}
isPanning = true;
this.focusContainer();
if (!this.state.editingTextElement) {
event.preventDefault();
}
let nextPastePrevented = false;
const isLinux = typeof window === void 0 ? false : /Linux/.test(window.navigator.platform);
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRABBING);
let { clientX: lastX, clientY: lastY } = event;
const onPointerMove = withBatchedUpdatesThrottled((event2) => {
const deltaX = lastX - event2.clientX;
const deltaY = lastY - event2.clientY;
lastX = event2.clientX;
lastY = event2.clientY;
if (isLinux && !nextPastePrevented && (Math.abs(deltaX) > 1 || Math.abs(deltaY) > 1)) {
nextPastePrevented = true;
const preventNextPaste = (event3) => {
document.body.removeEventListener("paste" /* PASTE */, preventNextPaste);
event3.stopPropagation();
};
const enableNextPaste = () => {
setTimeout(() => {
document.body.removeEventListener("paste" /* PASTE */, preventNextPaste);
window.removeEventListener("pointerup" /* POINTER_UP */, enableNextPaste);
}, 100);
};
document.body.addEventListener("paste" /* PASTE */, preventNextPaste);
window.addEventListener("pointerup" /* POINTER_UP */, enableNextPaste);
}
this.translateCanvas({
scrollX: this.state.scrollX - deltaX / this.state.zoom.value,
scrollY: this.state.scrollY - deltaY / this.state.zoom.value
});
});
const teardown = withBatchedUpdates(
lastPointerUp = () => {
lastPointerUp = null;
isPanning = false;
if (!isHoldingSpace) {
if (this.state.viewModeEnabled) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
} else {
setCursorForShape(this.interactiveCanvas, this.state);
}
}
this.setState({
cursorButton: "up"
});
this.savePointer(event.clientX, event.clientY, "up");
window.removeEventListener("pointermove" /* POINTER_MOVE */, onPointerMove);
window.removeEventListener("pointerup" /* POINTER_UP */, teardown);
window.removeEventListener("blur" /* BLUR */, teardown);
onPointerMove.flush();
}
);
window.addEventListener("blur" /* BLUR */, teardown);
window.addEventListener("pointermove" /* POINTER_MOVE */, onPointerMove, {
passive: true
});
window.addEventListener("pointerup" /* POINTER_UP */, teardown);
return true;
});
__publicField(this, "clearSelectionIfNotUsingSelection", () => {
if (this.state.activeTool.type !== "selection") {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedGroupIds: {},
editingGroupId: null,
activeEmbeddable: null
});
}
});
/**
* @returns whether the pointer event has been completely handled
*/
__publicField(this, "handleSelectionOnPointerDown", (event, pointerDownState) => {
if (this.state.activeTool.type === "selection") {
const elements = this.scene.getNonDeletedElements();
const elementsMap = this.scene.getNonDeletedElementsMap();
const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement && !isElbowArrow(selectedElements[0]) && !(this.state.selectedLinearElement && this.state.selectedLinearElement.hoverPointIndex !== -1)) {
const elementWithTransformHandleType = getElementWithTransformHandleType(
elements,
this.state,
pointerDownState.origin.x,
pointerDownState.origin.y,
this.state.zoom,
event.pointerType,
this.scene.getNonDeletedElementsMap(),
this.device
);
if (elementWithTransformHandleType != null) {
if (elementWithTransformHandleType.transformHandleType === "rotation") {
this.setState({
resizingElement: elementWithTransformHandleType.element
});
pointerDownState.resize.handleType = elementWithTransformHandleType.transformHandleType;
} else if (this.state.croppingElementId) {
pointerDownState.resize.handleType = elementWithTransformHandleType.transformHandleType;
} else {
this.setState({
resizingElement: elementWithTransformHandleType.element
});
pointerDownState.resize.handleType = elementWithTransformHandleType.transformHandleType;
}
}
} else if (selectedElements.length > 1) {
pointerDownState.resize.handleType = getTransformHandleTypeFromCoords(
getCommonBounds(selectedElements),
pointerDownState.origin.x,
pointerDownState.origin.y,
this.state.zoom,
event.pointerType,
this.device
);
}
if (pointerDownState.resize.handleType) {
pointerDownState.resize.isResizing = true;
pointerDownState.resize.offset = tupleToCoors(
getResizeOffsetXY(
pointerDownState.resize.handleType,
selectedElements,
elementsMap,
pointerDownState.origin.x,
pointerDownState.origin.y
)
);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0]) && selectedElements[0].points.length === 2) {
pointerDownState.resize.arrowDirection = getResizeArrowDirection(
pointerDownState.resize.handleType,
selectedElements[0]
);
}
} else {
if (this.state.selectedLinearElement) {
const linearElementEditor = this.state.editingLinearElement || this.state.selectedLinearElement;
const ret = LinearElementEditor.handlePointerDown(
event,
this,
this.store,
pointerDownState.origin,
linearElementEditor,
this.scene
);
if (ret.hitElement) {
pointerDownState.hit.element = ret.hitElement;
}
if (ret.linearElementEditor) {
this.setState({ selectedLinearElement: ret.linearElementEditor });
if (this.state.editingLinearElement) {
this.setState({ editingLinearElement: ret.linearElementEditor });
}
}
if (ret.didAddPoint) {
return true;
}
}
pointerDownState.hit.element = pointerDownState.hit.element ?? this.getElementAtPosition(
pointerDownState.origin.x,
pointerDownState.origin.y
);
this.hitLinkElement = this.getElementLinkAtPosition(
pointerDownState.origin,
pointerDownState.hit.element
);
if (this.hitLinkElement) {
return true;
}
if (this.state.croppingElementId && pointerDownState.hit.element?.id !== this.state.croppingElementId) {
this.finishImageCropping();
}
if (pointerDownState.hit.element) {
const hitLinkElement = this.getElementLinkAtPosition(
{
x: pointerDownState.origin.x,
y: pointerDownState.origin.y
},
pointerDownState.hit.element
);
if (hitLinkElement) {
return false;
}
}
pointerDownState.hit.allHitElements = this.getElementsAtPosition(
pointerDownState.origin.x,
pointerDownState.origin.y
);
const hitElement = pointerDownState.hit.element;
const someHitElementIsSelected = pointerDownState.hit.allHitElements.some(
(element) => this.isASelectedElement(element)
);
if ((hitElement === null || !someHitElementIsSelected) && !event.shiftKey && !pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) {
this.clearSelection(hitElement);
}
if (this.state.editingLinearElement) {
this.setState({
selectedElementIds: makeNextSelectedElementIds(
{
[this.state.editingLinearElement.elementId]: true
},
this.state
)
});
} else if (hitElement != null) {
if (event[KEYS.CTRL_OR_CMD]) {
if (!this.state.selectedElementIds[hitElement.id]) {
pointerDownState.hit.wasAddedToSelection = true;
}
this.setState((prevState) => ({
...editGroupForSelectedElement(prevState, hitElement),
previousSelectedElementIds: this.state.selectedElementIds
}));
return false;
}
if (!this.state.selectedElementIds[hitElement.id]) {
if (this.state.editingGroupId && !isElementInGroup(hitElement, this.state.editingGroupId)) {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedGroupIds: {},
editingGroupId: null,
activeEmbeddable: null
});
}
if (!someHitElementIsSelected && !pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) {
this.setState((prevState) => {
let nextSelectedElementIds = {
...prevState.selectedElementIds,
[hitElement.id]: true
};
const previouslySelectedElements = [];
Object.keys(prevState.selectedElementIds).forEach((id) => {
const element = this.scene.getElement(id);
element && previouslySelectedElements.push(element);
});
if (isFrameLikeElement(hitElement)) {
getFrameChildren(
previouslySelectedElements,
hitElement.id
).forEach((element) => {
delete nextSelectedElementIds[element.id];
});
} else if (hitElement.frameId) {
if (nextSelectedElementIds[hitElement.frameId]) {
delete nextSelectedElementIds[hitElement.id];
}
} else {
const groupIds = hitElement.groupIds;
const framesInGroups = new Set(
groupIds.flatMap(
(gid) => getElementsInGroup(
this.scene.getNonDeletedElements(),
gid
)
).filter((element) => isFrameLikeElement(element)).map((frame) => frame.id)
);
if (framesInGroups.size > 0) {
previouslySelectedElements.forEach((element) => {
if (element.frameId && framesInGroups.has(element.frameId)) {
delete nextSelectedElementIds[element.id];
element.groupIds.flatMap(
(gid) => getElementsInGroup(
this.scene.getNonDeletedElements(),
gid
)
).forEach((element2) => {
delete nextSelectedElementIds[element2.id];
});
}
});
}
}
if (prevState.openDialog?.name === "elementLinkSelector") {
if (!hitElement.groupIds.some(
(gid) => prevState.selectedGroupIds[gid]
)) {
nextSelectedElementIds = {
[hitElement.id]: true
};
}
}
return {
...selectGroupsForSelectedElements(
{
editingGroupId: prevState.editingGroupId,
selectedElementIds: nextSelectedElementIds
},
this.scene.getNonDeletedElements(),
prevState,
this
),
showHyperlinkPopup: hitElement.link || isEmbeddableElement(hitElement) ? "info" : false
};
});
pointerDownState.hit.wasAddedToSelection = true;
}
}
}
this.setState({
previousSelectedElementIds: this.state.selectedElementIds
});
}
}
return false;
});
__publicField(this, "handleTextOnPointerDown", (event, pointerDownState) => {
if (this.state.editingTextElement) {
return;
}
let sceneX = pointerDownState.origin.x;
let sceneY = pointerDownState.origin.y;
const element = this.getElementAtPosition(sceneX, sceneY, {
includeBoundTextElement: true
});
let container = this.getTextBindableContainerAtPosition(sceneX, sceneY);
if (hasBoundTextElement(element)) {
container = element;
sceneX = element.x + element.width / 2;
sceneY = element.y + element.height / 2;
}
this.startTextEditing({
sceneX,
sceneY,
insertAtParentCenter: !event.altKey,
container,
autoEdit: false
});
resetCursor(this.interactiveCanvas);
if (!this.state.activeTool.locked) {
this.setState({
activeTool: updateActiveTool(this.state, { type: "selection" })
});
}
});
__publicField(this, "handleFreeDrawElementOnPointerDown", (event, elementType, pointerDownState) => {
const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x,
pointerDownState.origin.y,
null
);
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x: gridX,
y: gridY
});
const simulatePressure = event.pressure === 0.5;
const element = newFreeDrawElement({
type: elementType,
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
roundness: null,
simulatePressure,
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null,
points: [pointFrom(0, 0)],
pressures: simulatePressure ? [] : [event.pressure]
});
this.scene.insertElement(element);
this.setState((prevState) => {
const nextSelectedElementIds = {
...prevState.selectedElementIds
};
delete nextSelectedElementIds[element.id];
return {
selectedElementIds: makeNextSelectedElementIds(
nextSelectedElementIds,
prevState
)
};
});
const boundElement = getHoveredElementForBinding(
pointerDownState.origin,
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom
);
this.setState({
newElement: element,
startBoundElement: boundElement,
suggestedBindings: []
});
});
__publicField(this, "insertIframeElement", ({
sceneX,
sceneY,
width,
height
}) => {
const [gridX, gridY] = getGridPoint(
sceneX,
sceneY,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const element = newIframeElement({
type: "iframe",
x: gridX,
y: gridY,
strokeColor: "transparent",
backgroundColor: "transparent",
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
roundness: this.getCurrentItemRoundness("iframe"),
opacity: this.state.currentItemOpacity,
locked: false,
width,
height
});
this.scene.insertElement(element);
return element;
});
//create rectangle element with youtube top left on nearest grid point width / hight 640/360
__publicField(this, "insertEmbeddableElement", ({
sceneX,
sceneY,
link
}) => {
const [gridX, gridY] = getGridPoint(
sceneX,
sceneY,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const embedLink = getEmbedLink(link);
if (!embedLink) {
return;
}
if (embedLink.error instanceof URIError) {
this.setToast({
message: t("toast.unrecognizedLinkFormat"),
closable: true
});
}
const element = newEmbeddableElement({
type: "embeddable",
x: gridX,
y: gridY,
strokeColor: "transparent",
backgroundColor: "transparent",
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
roundness: this.getCurrentItemRoundness("embeddable"),
opacity: this.state.currentItemOpacity,
locked: false,
width: embedLink.intrinsicSize.w,
height: embedLink.intrinsicSize.h,
link
});
this.scene.insertElement(element);
return element;
});
__publicField(this, "createImageElement", ({
sceneX,
sceneY,
addToFrameUnderCursor = true
}) => {
const [gridX, gridY] = getGridPoint(
sceneX,
sceneY,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const topLayerFrame = addToFrameUnderCursor ? this.getTopLayerFrameAtSceneCoords({
x: gridX,
y: gridY
}) : null;
const element = newImageElement({
type: "image",
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
roundness: null,
opacity: this.state.currentItemOpacity,
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null
});
return element;
});
__publicField(this, "handleLinearElementOnPointerDown", (event, elementType, pointerDownState) => {
if (this.state.multiElement) {
const { multiElement } = this.state;
if (multiElement.type === "line" && isPathALoop(multiElement.points, this.state.zoom.value)) {
mutateElement(multiElement, {
lastCommittedPoint: multiElement.points[multiElement.points.length - 1]
});
this.actionManager.executeAction(actionFinalize);
return;
}
if (isElbowArrow(multiElement) && multiElement.points.length > 1) {
mutateElement(multiElement, {
lastCommittedPoint: multiElement.points[multiElement.points.length - 1]
});
this.actionManager.executeAction(actionFinalize);
return;
}
const { x: rx, y: ry, lastCommittedPoint } = multiElement;
if (multiElement.points.length > 1 && lastCommittedPoint && pointDistance(
pointFrom(
pointerDownState.origin.x - rx,
pointerDownState.origin.y - ry
),
lastCommittedPoint
) < LINE_CONFIRM_THRESHOLD) {
this.actionManager.executeAction(actionFinalize);
return;
}
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[multiElement.id]: true
},
prevState
)
}));
mutateElement(multiElement, {
lastCommittedPoint: multiElement.points[multiElement.points.length - 1]
});
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
} else {
const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x,
pointerDownState.origin.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x: gridX,
y: gridY
});
const { currentItemStartArrowhead, currentItemEndArrowhead } = this.state;
const [startArrowhead, endArrowhead] = elementType === "arrow" ? [currentItemStartArrowhead, currentItemEndArrowhead] : [null, null];
const element = elementType === "arrow" ? newArrowElement({
type: elementType,
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
roundness: this.state.currentItemArrowType === ARROW_TYPE.round ? { type: ROUNDNESS.PROPORTIONAL_RADIUS } : (
// note, roundness doesn't have any effect for elbow arrows,
// but it's best to set it to null as well
null
),
startArrowhead,
endArrowhead,
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null,
elbowed: this.state.currentItemArrowType === ARROW_TYPE.elbow,
fixedSegments: this.state.currentItemArrowType === ARROW_TYPE.elbow ? [] : null
}) : newLinearElement({
type: elementType,
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
roundness: this.state.currentItemRoundness === "round" ? { type: ROUNDNESS.PROPORTIONAL_RADIUS } : null,
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null
});
this.setState((prevState) => {
const nextSelectedElementIds = {
...prevState.selectedElementIds
};
delete nextSelectedElementIds[element.id];
return {
selectedElementIds: makeNextSelectedElementIds(
nextSelectedElementIds,
prevState
)
};
});
mutateElement(element, {
points: [...element.points, pointFrom(0, 0)]
});
const boundElement = getHoveredElementForBinding(
pointerDownState.origin,
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
isElbowArrow(element),
isElbowArrow(element)
);
this.scene.insertElement(element);
this.setState({
newElement: element,
startBoundElement: boundElement,
suggestedBindings: []
});
}
});
__publicField(this, "createGenericElementOnPointerDown", (elementType, pointerDownState) => {
const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x,
pointerDownState.origin.y,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x: gridX,
y: gridY
});
const baseElementAttributes = {
x: gridX,
y: gridY,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
roundness: this.getCurrentItemRoundness(elementType),
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null
};
let element;
if (elementType === "embeddable") {
element = newEmbeddableElement({
type: "embeddable",
...baseElementAttributes
});
} else {
element = newElement({
type: elementType,
...baseElementAttributes
});
}
if (element.type === "selection") {
this.setState({
selectionElement: element
});
} else {
this.scene.insertElement(element);
this.setState({
multiElement: null,
newElement: element
});
}
});
__publicField(this, "createFrameElementOnPointerDown", (pointerDownState, type) => {
const [gridX, gridY] = getGridPoint(
pointerDownState.origin.x,
pointerDownState.origin.y,
this.lastPointerDownEvent?.[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const constructorOpts = {
x: gridX,
y: gridY,
opacity: this.state.currentItemOpacity,
locked: false,
...FRAME_STYLE
};
const frame = type === TOOL_TYPE.magicframe ? newMagicFrameElement(constructorOpts) : newFrameElement(constructorOpts);
this.scene.insertElement(frame);
this.setState({
multiElement: null,
newElement: frame
});
});
__publicField(this, "restoreReadyToEraseElements", () => {
this.elementsPendingErasure = /* @__PURE__ */ new Set();
this.triggerRender();
});
__publicField(this, "eraseElements", () => {
let didChange = false;
const elements = this.scene.getElementsIncludingDeleted().map((ele) => {
if (this.elementsPendingErasure.has(ele.id) || ele.frameId && this.elementsPendingErasure.has(ele.frameId) || isBoundToContainer(ele) && this.elementsPendingErasure.has(ele.containerId)) {
didChange = true;
return newElementWith(ele, { isDeleted: true });
}
return ele;
});
this.elementsPendingErasure = /* @__PURE__ */ new Set();
if (didChange) {
this.store.shouldCaptureIncrement();
this.scene.replaceAllElements(elements);
}
});
__publicField(this, "initializeImage", async ({
imageFile,
imageElement: _imageElement,
showCursorImagePreview = false
}) => {
if (!isSupportedImageFile(imageFile)) {
throw new Error(t("errors.unsupportedFileType"));
}
const mimeType = imageFile.type;
setCursor(this.interactiveCanvas, "wait");
if (mimeType === MIME_TYPES.svg) {
try {
imageFile = SVGStringToFile(
normalizeSVG(await imageFile.text()),
imageFile.name
);
} catch (error) {
console.warn(error);
throw new Error(t("errors.svgImageInsertError"));
}
}
const fileId = await (this.props.generateIdForFile?.(
imageFile
) || generateIdFromFile(imageFile));
if (!fileId) {
console.warn(
"Couldn't generate file id or the supplied `generateIdForFile` didn't resolve to one."
);
throw new Error(t("errors.imageInsertError"));
}
const existingFileData = this.files[fileId];
if (!existingFileData?.dataURL) {
try {
imageFile = await resizeImageFile(imageFile, {
maxWidthOrHeight: DEFAULT_MAX_IMAGE_WIDTH_OR_HEIGHT
});
} catch (error) {
console.error(
"Error trying to resizing image file on insertion",
error
);
}
if (imageFile.size > MAX_ALLOWED_FILE_BYTES) {
throw new Error(
t("errors.fileTooBig", {
maxSize: `${Math.trunc(MAX_ALLOWED_FILE_BYTES / 1024 / 1024)}MB`
})
);
}
}
if (showCursorImagePreview) {
const dataURL2 = this.files[fileId]?.dataURL;
const resizedFile = dataURL2 && dataURLToFile(dataURL2);
this.setImagePreviewCursor(resizedFile || imageFile);
}
const dataURL = this.files[fileId]?.dataURL || await getDataURL(imageFile);
const imageElement = mutateElement(
_imageElement,
{
fileId
},
false
);
return new Promise(
async (resolve, reject) => {
try {
this.addMissingFiles([
{
mimeType,
id: fileId,
dataURL,
created: Date.now(),
lastRetrieved: Date.now()
}
]);
const cachedImageData = this.imageCache.get(fileId);
if (!cachedImageData) {
this.addNewImagesToImageCache();
await this.updateImageCache([imageElement]);
}
if (cachedImageData?.image instanceof Promise) {
await cachedImageData.image;
}
if (this.state.pendingImageElementId !== imageElement.id && this.state.newElement?.id !== imageElement.id) {
this.initializeImageDimensions(imageElement, true);
}
resolve(imageElement);
} catch (error) {
console.error(error);
reject(new Error(t("errors.imageInsertError")));
} finally {
if (!showCursorImagePreview) {
resetCursor(this.interactiveCanvas);
}
}
}
);
});
/**
* inserts image into elements array and rerenders
*/
__publicField(this, "insertImageElement", async (imageElement, imageFile, showCursorImagePreview) => {
if (!this.isToolSupported("image")) {
this.setState({ errorMessage: t("errors.imageToolNotSupported") });
return;
}
this.scene.insertElement(imageElement);
try {
return await this.initializeImage({
imageFile,
imageElement,
showCursorImagePreview
});
} catch (error) {
mutateElement(imageElement, {
isDeleted: true
});
this.actionManager.executeAction(actionFinalize);
this.setState({
errorMessage: error.message || t("errors.imageInsertError")
});
return null;
}
});
__publicField(this, "setImagePreviewCursor", async (imageFile) => {
const cursorImageSizePx = 96;
let imagePreview;
try {
imagePreview = await resizeImageFile(imageFile, {
maxWidthOrHeight: cursorImageSizePx
});
} catch (e) {
if (e.cause === "UNSUPPORTED") {
throw new Error(t("errors.unsupportedFileType"));
}
throw e;
}
let previewDataURL = await getDataURL(imagePreview);
if (imageFile.type === MIME_TYPES.svg) {
const img = await loadHTMLImageElement(previewDataURL);
let height = Math.min(img.height, cursorImageSizePx);
let width = height * (img.width / img.height);
if (width > cursorImageSizePx) {
width = cursorImageSizePx;
height = width * (img.height / img.width);
}
const canvas = document.createElement("canvas");
canvas.height = height;
canvas.width = width;
const context = canvas.getContext("2d");
context.drawImage(img, 0, 0, width, height);
previewDataURL = canvas.toDataURL(MIME_TYPES.svg);
}
if (this.state.pendingImageElementId) {
setCursor(this.interactiveCanvas, `url(${previewDataURL}) 4 4, auto`);
}
});
__publicField(this, "onImageAction", async ({
insertOnCanvasDirectly
}) => {
try {
const clientX = this.state.width / 2 + this.state.offsetLeft;
const clientY = this.state.height / 2 + this.state.offsetTop;
const { x, y } = viewportCoordsToSceneCoords(
{ clientX, clientY },
this.state
);
const imageFile = await fileOpen({
description: "Image",
extensions: Object.keys(
IMAGE_MIME_TYPES
)
});
const imageElement = this.createImageElement({
sceneX: x,
sceneY: y,
addToFrameUnderCursor: false
});
if (insertOnCanvasDirectly) {
this.insertImageElement(imageElement, imageFile);
this.initializeImageDimensions(imageElement);
this.setState(
{
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state
)
},
() => {
this.actionManager.executeAction(actionFinalize);
}
);
} else {
this.setState(
{
pendingImageElementId: imageElement.id
},
() => {
this.insertImageElement(
imageElement,
imageFile,
/* showCursorImagePreview */
true
);
}
);
}
} catch (error) {
if (error.name !== "AbortError") {
console.error(error);
} else {
console.warn(error);
}
this.setState(
{
pendingImageElementId: null,
newElement: null,
activeTool: updateActiveTool(this.state, { type: "selection" })
},
() => {
this.actionManager.executeAction(actionFinalize);
}
);
}
});
__publicField(this, "initializeImageDimensions", (imageElement, forceNaturalSize = false) => {
const image = isInitializedImageElement(imageElement) && this.imageCache.get(imageElement.fileId)?.image;
if (!image || image instanceof Promise) {
if (imageElement.width < DRAGGING_THRESHOLD / this.state.zoom.value && imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value) {
const placeholderSize = 100 / this.state.zoom.value;
mutateElement(imageElement, {
x: imageElement.x - placeholderSize / 2,
y: imageElement.y - placeholderSize / 2,
width: placeholderSize,
height: placeholderSize
});
}
return;
}
if (forceNaturalSize || // if user-created bounding box is below threshold, assume the
// intention was to click instead of drag, and use the image's
// intrinsic size
imageElement.width < DRAGGING_THRESHOLD / this.state.zoom.value && imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value) {
const minHeight = Math.max(this.state.height - 120, 160);
const maxHeight = Math.min(
minHeight,
Math.floor(this.state.height * 0.5) / this.state.zoom.value
);
const height = Math.min(image.naturalHeight, maxHeight);
const width = height * (image.naturalWidth / image.naturalHeight);
const x = imageElement.x + imageElement.width / 2 - width / 2;
const y = imageElement.y + imageElement.height / 2 - height / 2;
mutateElement(imageElement, {
x,
y,
width,
height,
crop: null
});
}
});
/** updates image cache, refreshing updated elements and/or setting status
to error for images that fail during <img> element creation */
__publicField(this, "updateImageCache", async (elements, files = this.files) => {
const { updatedFiles, erroredFiles } = await updateImageCache({
imageCache: this.imageCache,
fileIds: elements.map((element) => element.fileId),
files
});
if (updatedFiles.size || erroredFiles.size) {
for (const element of elements) {
if (updatedFiles.has(element.fileId)) {
ShapeCache.delete(element);
}
}
}
if (erroredFiles.size) {
this.scene.replaceAllElements(
this.scene.getElementsIncludingDeleted().map((element) => {
if (isInitializedImageElement(element) && erroredFiles.has(element.fileId)) {
return newElementWith(element, {
status: "error"
});
}
return element;
})
);
}
return { updatedFiles, erroredFiles };
});
/** adds new images to imageCache and re-renders if needed */
__publicField(this, "addNewImagesToImageCache", async (imageElements = getInitializedImageElements(
this.scene.getNonDeletedElements()
), files = this.files) => {
const uncachedImageElements = imageElements.filter(
(element) => !element.isDeleted && !this.imageCache.has(element.fileId)
);
if (uncachedImageElements.length) {
const { updatedFiles } = await this.updateImageCache(
uncachedImageElements,
files
);
if (updatedFiles.size) {
this.scene.triggerUpdate();
}
}
});
/** generally you should use `addNewImagesToImageCache()` directly if you need
* to render new images. This is just a failsafe */
__publicField(this, "scheduleImageRefresh", throttle3(() => {
this.addNewImagesToImageCache();
}, IMAGE_RENDER_TIMEOUT));
__publicField(this, "updateBindingEnabledOnPointerMove", (event) => {
const shouldEnableBinding = shouldEnableBindingForPointerEvent(event);
if (this.state.isBindingEnabled !== shouldEnableBinding) {
this.setState({ isBindingEnabled: shouldEnableBinding });
}
});
__publicField(this, "maybeSuggestBindingAtCursor", (pointerCoords, considerAll) => {
const hoveredBindableElement = getHoveredElementForBinding(
pointerCoords,
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
false,
considerAll
);
this.setState({
suggestedBindings: hoveredBindableElement != null ? [hoveredBindableElement] : []
});
});
__publicField(this, "maybeSuggestBindingsForLinearElementAtCoords", (linearElement, pointerCoords, oppositeBindingBoundElement) => {
if (!pointerCoords.length) {
return;
}
const suggestedBindings = pointerCoords.reduce(
(acc, coords) => {
const hoveredBindableElement = getHoveredElementForBinding(
coords,
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
this.state.zoom,
isElbowArrow(linearElement),
isElbowArrow(linearElement)
);
if (hoveredBindableElement != null && !isLinearElementSimpleAndAlreadyBound(
linearElement,
oppositeBindingBoundElement?.id,
hoveredBindableElement
)) {
acc.push(hoveredBindableElement);
}
return acc;
},
[]
);
this.setState({ suggestedBindings });
});
__publicField(this, "handleInteractiveCanvasRef", (canvas) => {
if (canvas !== null) {
this.interactiveCanvas = canvas;
this.interactiveCanvas.addEventListener(
"touchstart" /* TOUCH_START */,
this.onTouchStart,
{ passive: false }
);
this.interactiveCanvas.addEventListener("touchend" /* TOUCH_END */, this.onTouchEnd);
} else {
this.interactiveCanvas?.removeEventListener(
"touchstart" /* TOUCH_START */,
this.onTouchStart
);
this.interactiveCanvas?.removeEventListener(
"touchend" /* TOUCH_END */,
this.onTouchEnd
);
}
});
__publicField(this, "handleAppOnDrop", async (event) => {
const { file, fileHandle } = await getFileFromEvent(event);
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
event,
this.state
);
try {
if (isSupportedImageFile(file) && this.isToolSupported("image")) {
if (file?.type === MIME_TYPES.png || file?.type === MIME_TYPES.svg) {
try {
const scene = await loadFromBlob(
file,
this.state,
this.scene.getElementsIncludingDeleted(),
fileHandle
);
this.syncActionResult({
...scene,
appState: {
...scene.appState || this.state,
isLoading: false
},
replaceFiles: true,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
});
return;
} catch (error) {
if (error.name !== "EncodingError") {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
}
}
const imageElement = this.createImageElement({ sceneX, sceneY });
this.insertImageElement(imageElement, file);
this.initializeImageDimensions(imageElement);
this.setState({
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state
)
});
return;
}
} catch (error) {
return this.setState({
isLoading: false,
errorMessage: error.message
});
}
const libraryJSON = event.dataTransfer.getData(MIME_TYPES.excalidrawlib);
if (libraryJSON && typeof libraryJSON === "string") {
try {
const libraryItems = parseLibraryJSON(libraryJSON);
this.addElementsFromPasteOrLibrary({
elements: distributeLibraryItemsOnSquareGrid(libraryItems),
position: event,
files: null
});
} catch (error) {
this.setState({ errorMessage: error.message });
}
return;
}
if (file) {
await this.loadFileToCanvas(file, fileHandle);
}
if (event.dataTransfer?.types?.includes("text/plain")) {
const text = event.dataTransfer?.getData("text");
if (text && embeddableURLValidator(text, this.props.validateEmbeddable) && (/^(http|https):\/\/[^\s/$.?#].[^\s]*$/.test(text) || getEmbedLink(text)?.type === "video")) {
const embeddable = this.insertEmbeddableElement({
sceneX,
sceneY,
link: normalizeLink(text)
});
if (embeddable) {
this.setState({ selectedElementIds: { [embeddable.id]: true } });
}
}
}
});
__publicField(this, "loadFileToCanvas", async (file, fileHandle) => {
file = await normalizeFile(file);
try {
const elements = this.scene.getElementsIncludingDeleted();
let ret;
try {
ret = await loadSceneOrLibraryFromBlob(
file,
this.state,
elements,
fileHandle
);
} catch (error) {
const imageSceneDataError = error instanceof ImageSceneDataError;
if (imageSceneDataError && error.code === "IMAGE_NOT_CONTAINS_SCENE_DATA" && !this.isToolSupported("image")) {
this.setState({
isLoading: false,
errorMessage: t("errors.imageToolNotSupported")
});
return;
}
const errorMessage = imageSceneDataError ? t("alerts.cannotRestoreFromImage") : t("alerts.couldNotLoadInvalidFile");
this.setState({
isLoading: false,
errorMessage
});
}
if (!ret) {
return;
}
if (ret.type === MIME_TYPES.excalidraw) {
syncInvalidIndices(elements.concat(ret.data.elements));
this.store.updateSnapshot(arrayToMap(elements), this.state);
this.setState({ isLoading: true });
this.syncActionResult({
...ret.data,
appState: {
...ret.data.appState || this.state,
isLoading: false
},
replaceFiles: true,
captureUpdate: CaptureUpdateAction.IMMEDIATELY
});
} else if (ret.type === MIME_TYPES.excalidrawlib) {
await this.library.updateLibrary({
libraryItems: file,
merge: true,
openLibraryMenu: true
}).catch((error) => {
console.error(error);
this.setState({ errorMessage: t("errors.importLibraryError") });
});
}
} catch (error) {
this.setState({ isLoading: false, errorMessage: error.message });
}
});
__publicField(this, "handleCanvasContextMenu", (event) => {
event.preventDefault();
if (("pointerType" in event.nativeEvent && event.nativeEvent.pointerType === "touch" || "pointerType" in event.nativeEvent && event.nativeEvent.pointerType === "pen" && // always allow if user uses a pen secondary button
event.button !== POINTER_BUTTON.SECONDARY) && this.state.activeTool.type !== "selection") {
return;
}
const { x, y } = viewportCoordsToSceneCoords(event, this.state);
const element = this.getElementAtPosition(x, y, {
preferSelected: true,
includeLockedElements: true
});
const selectedElements = this.scene.getSelectedElements(this.state);
const isHittingCommonBoundBox = this.isHittingCommonBoundingBoxOfSelectedElements(
{ x, y },
selectedElements
);
const type = element || isHittingCommonBoundBox ? "element" : "canvas";
const container = this.excalidrawContainerRef.current;
const { top: offsetTop, left: offsetLeft } = container.getBoundingClientRect();
const left = event.clientX - offsetLeft;
const top = event.clientY - offsetTop;
trackEvent("contextMenu", "openContextMenu", type);
this.setState(
{
...element && !this.state.selectedElementIds[element.id] ? {
...this.state,
...selectGroupsForSelectedElements(
{
editingGroupId: this.state.editingGroupId,
selectedElementIds: { [element.id]: true }
},
this.scene.getNonDeletedElements(),
this.state,
this
),
selectedLinearElement: isLinearElement(element) ? new LinearElementEditor(element) : null
} : this.state,
showHyperlinkPopup: false
},
() => {
this.setState({
contextMenu: { top, left, items: this.getContextMenuItems(type) }
});
}
);
});
__publicField(this, "maybeDragNewGenericElement", (pointerDownState, event, informMutation = true) => {
const selectionElement = this.state.selectionElement;
const pointerCoords = pointerDownState.lastCoords;
if (selectionElement && this.state.activeTool.type !== "eraser") {
dragNewElement({
newElement: selectionElement,
elementType: this.state.activeTool.type,
originX: pointerDownState.origin.x,
originY: pointerDownState.origin.y,
x: pointerCoords.x,
y: pointerCoords.y,
width: distance(pointerDownState.origin.x, pointerCoords.x),
height: distance(pointerDownState.origin.y, pointerCoords.y),
shouldMaintainAspectRatio: shouldMaintainAspectRatio(event),
shouldResizeFromCenter: shouldResizeFromCenter(event),
zoom: this.state.zoom.value,
informMutation
});
return;
}
const newElement2 = this.state.newElement;
if (!newElement2) {
return;
}
let [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const image = isInitializedImageElement(newElement2) && this.imageCache.get(newElement2.fileId)?.image;
const aspectRatio = image && !(image instanceof Promise) ? image.width / image.height : null;
this.maybeCacheReferenceSnapPoints(event, [newElement2]);
const { snapOffset, snapLines } = snapNewElement(
newElement2,
this,
event,
{
x: pointerDownState.originInGrid.x + (this.state.originSnapOffset?.x ?? 0),
y: pointerDownState.originInGrid.y + (this.state.originSnapOffset?.y ?? 0)
},
{
x: gridX - pointerDownState.originInGrid.x,
y: gridY - pointerDownState.originInGrid.y
},
this.scene.getNonDeletedElementsMap()
);
gridX += snapOffset.x;
gridY += snapOffset.y;
this.setState({
snapLines
});
dragNewElement({
newElement: newElement2,
elementType: this.state.activeTool.type,
originX: pointerDownState.originInGrid.x,
originY: pointerDownState.originInGrid.y,
x: gridX,
y: gridY,
width: distance(pointerDownState.originInGrid.x, gridX),
height: distance(pointerDownState.originInGrid.y, gridY),
shouldMaintainAspectRatio: isImageElement(newElement2) ? !shouldMaintainAspectRatio(event) : shouldMaintainAspectRatio(event),
shouldResizeFromCenter: shouldResizeFromCenter(event),
zoom: this.state.zoom.value,
widthAspectRatio: aspectRatio,
originOffset: this.state.originSnapOffset,
informMutation
});
this.setState({
newElement: newElement2
});
if (this.state.activeTool.type === TOOL_TYPE.frame || this.state.activeTool.type === TOOL_TYPE.magicframe) {
this.setState({
elementsToHighlight: getElementsInResizingFrame(
this.scene.getNonDeletedElements(),
newElement2,
this.state,
this.scene.getNonDeletedElementsMap()
)
});
}
});
__publicField(this, "maybeHandleCrop", (pointerDownState, event) => {
if (!this.state.croppingElementId) {
return false;
}
const transformHandleType = pointerDownState.resize.handleType;
const pointerCoords = pointerDownState.lastCoords;
const [x, y] = getGridPoint(
pointerCoords.x - pointerDownState.resize.offset.x,
pointerCoords.y - pointerDownState.resize.offset.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const croppingElement = this.scene.getNonDeletedElementsMap().get(this.state.croppingElementId);
if (transformHandleType && croppingElement && isImageElement(croppingElement)) {
const croppingAtStateStart = pointerDownState.originalElements.get(
croppingElement.id
);
const image = isInitializedImageElement(croppingElement) && this.imageCache.get(croppingElement.fileId)?.image;
if (croppingAtStateStart && isImageElement(croppingAtStateStart) && image && !(image instanceof Promise)) {
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const dragOffset = {
x: gridX - pointerDownState.originInGrid.x,
y: gridY - pointerDownState.originInGrid.y
};
this.maybeCacheReferenceSnapPoints(event, [croppingElement]);
const { snapOffset, snapLines } = snapResizingElements(
[croppingElement],
[croppingAtStateStart],
this,
event,
dragOffset,
transformHandleType
);
mutateElement(
croppingElement,
cropElement(
croppingElement,
transformHandleType,
image.naturalWidth,
image.naturalHeight,
x + snapOffset.x,
y + snapOffset.y,
event.shiftKey ? croppingAtStateStart.width / croppingAtStateStart.height : void 0
)
);
updateBoundElements(
croppingElement,
this.scene.getNonDeletedElementsMap(),
{
newSize: {
width: croppingElement.width,
height: croppingElement.height
}
}
);
this.setState({
isCropping: transformHandleType && transformHandleType !== "rotation",
snapLines
});
}
return true;
}
return false;
});
__publicField(this, "maybeHandleResize", (pointerDownState, event) => {
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedFrames = selectedElements.filter(
(element) => isFrameLikeElement(element)
);
const transformHandleType = pointerDownState.resize.handleType;
if (
// Frames cannot be rotated.
selectedFrames.length > 0 && transformHandleType === "rotation" || // Elbow arrows cannot be transformed (resized or rotated).
selectedElements.length === 1 && isElbowArrow(selectedElements[0]) || // Do not resize when in crop mode
this.state.croppingElementId
) {
return false;
}
this.setState({
// TODO: rename this state field to "isScaling" to distinguish
// it from the generic "isResizing" which includes scaling and
// rotating
isResizing: transformHandleType && transformHandleType !== "rotation",
isRotating: transformHandleType === "rotation",
activeEmbeddable: null
});
const pointerCoords = pointerDownState.lastCoords;
let [resizeX, resizeY] = getGridPoint(
pointerCoords.x - pointerDownState.resize.offset.x,
pointerCoords.y - pointerDownState.resize.offset.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const frameElementsOffsetsMap = /* @__PURE__ */ new Map();
selectedFrames.forEach((frame) => {
const elementsInFrame = getFrameChildren(
this.scene.getNonDeletedElements(),
frame.id
);
elementsInFrame.forEach((element) => {
frameElementsOffsetsMap.set(frame.id + element.id, {
x: element.x - frame.x,
y: element.y - frame.y
});
});
});
if (!this.state.selectedElementsAreBeingDragged) {
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
const dragOffset = {
x: gridX - pointerDownState.originInGrid.x,
y: gridY - pointerDownState.originInGrid.y
};
const originalElements = [...pointerDownState.originalElements.values()];
this.maybeCacheReferenceSnapPoints(event, selectedElements);
const { snapOffset, snapLines } = snapResizingElements(
selectedElements,
getSelectedElements(originalElements, this.state),
this,
event,
dragOffset,
transformHandleType
);
resizeX += snapOffset.x;
resizeY += snapOffset.y;
this.setState({
snapLines
});
}
if (transformElements(
pointerDownState.originalElements,
transformHandleType,
selectedElements,
this.scene.getElementsMapIncludingDeleted(),
this.scene,
shouldRotateWithDiscreteAngle(event),
shouldResizeFromCenter(event),
selectedElements.some((element) => isImageElement(element)) ? !shouldMaintainAspectRatio(event) : shouldMaintainAspectRatio(event),
resizeX,
resizeY,
pointerDownState.resize.center.x,
pointerDownState.resize.center.y
)) {
const suggestedBindings = getSuggestedBindingsForArrows(
selectedElements,
this.scene.getNonDeletedElementsMap(),
this.state.zoom
);
const elementsToHighlight = /* @__PURE__ */ new Set();
selectedFrames.forEach((frame) => {
getElementsInResizingFrame(
this.scene.getNonDeletedElements(),
frame,
this.state,
this.scene.getNonDeletedElementsMap()
).forEach((element) => elementsToHighlight.add(element));
});
this.setState({
elementsToHighlight: [...elementsToHighlight],
suggestedBindings
});
return true;
}
return false;
});
__publicField(this, "getContextMenuItems", (type) => {
const options = [];
options.push(actionCopyAsPng, actionCopyAsSvg);
if (type === "canvas") {
if (this.state.viewModeEnabled) {
return [
...options,
actionToggleGridMode,
actionToggleZenMode,
actionToggleViewMode,
actionToggleStats
];
}
return [
actionPaste,
CONTEXT_MENU_SEPARATOR,
actionCopyAsPng,
actionCopyAsSvg,
copyText,
CONTEXT_MENU_SEPARATOR,
actionSelectAll,
actionUnlockAllElements,
CONTEXT_MENU_SEPARATOR,
actionToggleGridMode,
actionToggleObjectsSnapMode,
actionToggleZenMode,
actionToggleViewMode,
actionToggleStats
];
}
options.push(copyText);
if (this.state.viewModeEnabled) {
return [actionCopy, ...options];
}
return [
CONTEXT_MENU_SEPARATOR,
actionCut,
actionCopy,
actionPaste,
CONTEXT_MENU_SEPARATOR,
actionSelectAllElementsInFrame,
actionRemoveAllElementsFromFrame,
actionWrapSelectionInFrame,
CONTEXT_MENU_SEPARATOR,
actionToggleCropEditor,
CONTEXT_MENU_SEPARATOR,
...options,
CONTEXT_MENU_SEPARATOR,
actionCopyStyles,
actionPasteStyles,
CONTEXT_MENU_SEPARATOR,
actionGroup,
actionTextAutoResize,
actionUnbindText,
actionBindText,
actionWrapTextInContainer,
actionUngroup,
CONTEXT_MENU_SEPARATOR,
actionAddToLibrary,
CONTEXT_MENU_SEPARATOR,
actionSendBackward,
actionBringForward,
actionSendToBack,
actionBringToFront,
CONTEXT_MENU_SEPARATOR,
actionFlipHorizontal,
actionFlipVertical,
CONTEXT_MENU_SEPARATOR,
actionToggleLinearEditor,
CONTEXT_MENU_SEPARATOR,
actionLink,
actionCopyElementLink,
CONTEXT_MENU_SEPARATOR,
actionDuplicateSelection,
actionToggleElementLock,
CONTEXT_MENU_SEPARATOR,
actionDeleteSelected
];
});
__publicField(this, "handleWheel", withBatchedUpdates(
(event) => {
if (!(event.target instanceof HTMLCanvasElement || event.target instanceof HTMLTextAreaElement || event.target instanceof HTMLIFrameElement)) {
if (event[KEYS.CTRL_OR_CMD]) {
event.preventDefault();
}
return;
}
event.preventDefault();
if (isPanning) {
return;
}
const { deltaX, deltaY } = event;
if (event.metaKey || event.ctrlKey) {
const sign = Math.sign(deltaY);
const MAX_STEP = ZOOM_STEP * 100;
const absDelta = Math.abs(deltaY);
let delta = deltaY;
if (absDelta > MAX_STEP) {
delta = MAX_STEP * sign;
}
let newZoom = this.state.zoom.value - delta / 100;
newZoom += Math.log10(Math.max(1, this.state.zoom.value)) * -sign * // reduced amplification for small deltas (small movements on a trackpad)
Math.min(1, absDelta / 20);
this.translateCanvas((state) => ({
...getStateForZoom(
{
viewportX: this.lastViewportPosition.x,
viewportY: this.lastViewportPosition.y,
nextZoom: getNormalizedZoom(newZoom)
},
state
),
shouldCacheIgnoreZoom: true
}));
this.resetShouldCacheIgnoreZoomDebounced();
return;
}
if (event.shiftKey) {
this.translateCanvas(({ zoom, scrollX }) => ({
// on Mac, shift+wheel tends to result in deltaX
scrollX: scrollX - (deltaY || deltaX) / zoom.value
}));
return;
}
this.translateCanvas(({ zoom, scrollX, scrollY }) => ({
scrollX: scrollX - deltaX / zoom.value,
scrollY: scrollY - deltaY / zoom.value
}));
}
));
__publicField(this, "savePointer", (x, y, button) => {
if (!x || !y) {
return;
}
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
{ clientX: x, clientY: y },
this.state
);
if (isNaN(sceneX) || isNaN(sceneY)) {
}
const pointer = {
x: sceneX,
y: sceneY,
tool: this.state.activeTool.type === "laser" ? "laser" : "pointer"
};
this.props.onPointerUpdate?.({
pointer,
button,
pointersMap: gesture.pointers
});
});
__publicField(this, "resetShouldCacheIgnoreZoomDebounced", debounce(() => {
if (!this.unmounted) {
this.setState({ shouldCacheIgnoreZoom: false });
}
}, 300));
__publicField(this, "updateDOMRect", (cb) => {
if (this.excalidrawContainerRef?.current) {
const excalidrawContainer = this.excalidrawContainerRef.current;
const {
width,
height,
left: offsetLeft,
top: offsetTop
} = excalidrawContainer.getBoundingClientRect();
const {
width: currentWidth,
height: currentHeight,
offsetTop: currentOffsetTop,
offsetLeft: currentOffsetLeft
} = this.state;
if (width === currentWidth && height === currentHeight && offsetLeft === currentOffsetLeft && offsetTop === currentOffsetTop) {
if (cb) {
cb();
}
return;
}
this.setState(
{
width,
height,
offsetLeft,
offsetTop
},
() => {
cb && cb();
}
);
}
});
__publicField(this, "refresh", () => {
this.setState({ ...this.getCanvasOffsets() });
});
const defaultAppState = getDefaultAppState();
const {
excalidrawAPI,
viewModeEnabled = false,
zenModeEnabled = false,
gridModeEnabled = false,
objectsSnapModeEnabled = false,
theme = defaultAppState.theme,
name = `${t("labels.untitled")}-${getDateTime()}`
} = props;
this.state = {
...defaultAppState,
theme,
isLoading: true,
...this.getCanvasOffsets(),
viewModeEnabled,
zenModeEnabled,
objectsSnapModeEnabled,
gridModeEnabled: gridModeEnabled ?? defaultAppState.gridModeEnabled,
name,
width: window.innerWidth,
height: window.innerHeight
};
this.id = nanoid();
this.library = new library_default(this);
this.actionManager = new ActionManager(
this.syncActionResult,
() => this.state,
() => this.scene.getElementsIncludingDeleted(),
this
);
this.scene = new Scene_default();
this.canvas = document.createElement("canvas");
this.rc = rough.canvas(this.canvas);
this.renderer = new Renderer(this.scene);
this.visibleElements = [];
this.store = new Store();
this.history = new History();
if (excalidrawAPI) {
const api = {
updateScene: this.updateScene,
updateLibrary: this.library.updateLibrary,
addFiles: this.addFiles,
resetScene: this.resetScene,
getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted,
history: {
clear: this.resetHistory
},
scrollToContent: this.scrollToContent,
getSceneElements: this.getSceneElements,
getAppState: () => this.state,
getFiles: () => this.files,
getName: this.getName,
registerAction: (action) => {
this.actionManager.registerAction(action);
},
refresh: this.refresh,
setToast: this.setToast,
id: this.id,
setActiveTool: this.setActiveTool,
setCursor: this.setCursor,
resetCursor: this.resetCursor,
updateFrameRendering: this.updateFrameRendering,
toggleSidebar: this.toggleSidebar,
onChange: (cb) => this.onChangeEmitter.on(cb),
onPointerDown: (cb) => this.onPointerDownEmitter.on(cb),
onPointerUp: (cb) => this.onPointerUpEmitter.on(cb),
onScrollChange: (cb) => this.onScrollChangeEmitter.on(cb),
onUserFollow: (cb) => this.onUserFollowEmitter.on(cb)
};
if (typeof excalidrawAPI === "function") {
excalidrawAPI(api);
} else {
console.error("excalidrawAPI should be a function!");
}
}
this.excalidrawContainerValue = {
container: this.excalidrawContainerRef.current,
id: this.id
};
this.fonts = new Fonts(this.scene);
this.history = new History();
this.actionManager.registerAll(actions);
this.actionManager.registerAction(
createUndoAction(this.history, this.store)
);
this.actionManager.registerAction(
createRedoAction(this.history, this.store)
);
}
onWindowMessage(event) {
if (event.origin !== "https://player.vimeo.com" && event.origin !== "https://www.youtube.com") {
return;
}
let data = null;
try {
data = JSON.parse(event.data);
} catch (e) {
}
if (!data) {
return;
}
switch (event.origin) {
case "https://player.vimeo.com":
if (data.method === "paused") {
let source = null;
const iframes = document.body.querySelectorAll(
"iframe.excalidraw__embeddable"
);
if (!iframes) {
break;
}
for (const iframe of iframes) {
if (iframe.contentWindow === event.source) {
source = iframe.contentWindow;
}
}
source?.postMessage(
JSON.stringify({
method: data.value ? "play" : "pause",
value: true
}),
"*"
);
}
break;
case "https://www.youtube.com":
if (data.event === "infoDelivery" && data.info && data.id && typeof data.info.playerState === "number") {
const id = data.id;
const playerState = data.info.playerState;
if (Object.values(YOUTUBE_STATES).includes(playerState)) {
YOUTUBE_VIDEO_STATES.set(
id,
playerState
);
}
}
break;
}
}
cacheEmbeddableRef(element, ref) {
if (ref) {
this.iFrameRefs.set(element.id, ref);
}
}
getHTMLIFrameElement(element) {
return this.iFrameRefs.get(element.id);
}
handleEmbeddableCenterClick(element) {
if (this.state.activeEmbeddable?.element === element && this.state.activeEmbeddable?.state === "active") {
return;
}
setTimeout(() => {
this.setState({
activeEmbeddable: { element, state: "active" },
selectedElementIds: { [element.id]: true },
newElement: null,
selectionElement: null
});
}, 100);
if (isIframeElement(element)) {
return;
}
const iframe = this.getHTMLIFrameElement(element);
if (!iframe?.contentWindow) {
return;
}
if (iframe.src.includes("youtube")) {
const state = YOUTUBE_VIDEO_STATES.get(element.id);
if (!state) {
YOUTUBE_VIDEO_STATES.set(element.id, YOUTUBE_STATES.UNSTARTED);
iframe.contentWindow.postMessage(
JSON.stringify({
event: "listening",
id: element.id
}),
"*"
);
}
switch (state) {
case YOUTUBE_STATES.PLAYING:
case YOUTUBE_STATES.BUFFERING:
iframe.contentWindow?.postMessage(
JSON.stringify({
event: "command",
func: "pauseVideo",
args: ""
}),
"*"
);
break;
default:
iframe.contentWindow?.postMessage(
JSON.stringify({
event: "command",
func: "playVideo",
args: ""
}),
"*"
);
}
}
if (iframe.src.includes("player.vimeo.com")) {
iframe.contentWindow.postMessage(
JSON.stringify({
method: "paused"
//video play/pause in onWindowMessage handler
}),
"*"
);
}
}
isIframeLikeElementCenter(el, event, sceneX, sceneY) {
return el && !event.altKey && !event.shiftKey && !event.metaKey && !event.ctrlKey && (this.state.activeEmbeddable?.element !== el || this.state.activeEmbeddable?.state === "hover" || !this.state.activeEmbeddable) && sceneX >= el.x + el.width / 3 && sceneX <= el.x + 2 * el.width / 3 && sceneY >= el.y + el.height / 3 && sceneY <= el.y + 2 * el.height / 3;
}
renderEmbeddables() {
const scale = this.state.zoom.value;
const normalizedWidth = this.state.width;
const normalizedHeight = this.state.height;
const embeddableElements = this.scene.getNonDeletedElements().filter(
(el) => isEmbeddableElement(el) && this.embedsValidationStatus.get(el.id) === true || isIframeElement(el)
);
return /* @__PURE__ */ jsx147(Fragment23, { children: embeddableElements.map((el) => {
const { x, y } = sceneCoordsToViewportCoords(
{ sceneX: el.x, sceneY: el.y },
this.state
);
const isVisible = isElementInViewport(
el,
normalizedWidth,
normalizedHeight,
this.state,
this.scene.getNonDeletedElementsMap()
);
const hasBeenInitialized = this.initializedEmbeds.has(el.id);
if (isVisible && !hasBeenInitialized) {
this.initializedEmbeds.add(el.id);
}
const shouldRender = isVisible || hasBeenInitialized;
if (!shouldRender) {
return null;
}
let src;
if (isIframeElement(el)) {
src = null;
const data = (el.customData?.generationData ?? this.magicGenerations.get(el.id)) || {
status: "error",
message: "No generation data",
code: "ERR_NO_GENERATION_DATA"
};
if (data.status === "done") {
const html = data.html;
src = {
intrinsicSize: { w: el.width, h: el.height },
type: "document",
srcdoc: () => {
return html;
}
};
} else if (data.status === "pending") {
src = {
intrinsicSize: { w: el.width, h: el.height },
type: "document",
srcdoc: () => {
return createSrcDoc(`
<style>
html, body {
width: 100%;
height: 100%;
color: ${this.state.theme === THEME.DARK ? "white" : "black"};
}
body {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
gap: 1rem;
}
.Spinner {
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
margin-right: auto;
}
.Spinner svg {
animation: rotate 1.6s linear infinite;
transform-origin: center center;
width: 40px;
height: 40px;
}
.Spinner circle {
stroke: currentColor;
animation: dash 1.6s linear 0s infinite;
stroke-linecap: round;
}
@keyframes rotate {
100% {
transform: rotate(360deg);
}
}
@keyframes dash {
0% {
stroke-dasharray: 1, 300;
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 150, 300;
stroke-dashoffset: -200;
}
100% {
stroke-dasharray: 1, 300;
stroke-dashoffset: -280;
}
}
</style>
<div class="Spinner">
<svg
viewBox="0 0 100 100"
>
<circle
cx="50"
cy="50"
r="46"
stroke-width="8"
fill="none"
stroke-miter-limit="10"
/>
</svg>
</div>
<div>Generating...</div>
`);
}
};
} else {
let message;
if (data.code === "ERR_GENERATION_INTERRUPTED") {
message = "Generation was interrupted...";
} else {
message = data.message || "Generation failed";
}
src = {
intrinsicSize: { w: el.width, h: el.height },
type: "document",
srcdoc: () => {
return createSrcDoc(`
<style>
html, body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: ${COLOR_PALETTE.red[3]};
}
h1, h3 {
margin-top: 0;
margin-bottom: 0.5rem;
}
</style>
<h1>Error!</h1>
<h3>${message}</h3>
`);
}
};
}
} else {
src = getEmbedLink(toValidURL(el.link || ""));
}
const isActive = this.state.activeEmbeddable?.element === el && this.state.activeEmbeddable?.state === "active";
const isHovered = this.state.activeEmbeddable?.element === el && this.state.activeEmbeddable?.state === "hover";
return /* @__PURE__ */ jsx147(
"div",
{
className: clsx55("excalidraw__embeddable-container", {
"is-hovered": isHovered
}),
style: {
transform: isVisible ? `translate(${x - this.state.offsetLeft}px, ${y - this.state.offsetTop}px) scale(${scale})` : "none",
display: isVisible ? "block" : "none",
opacity: getRenderOpacity(
el,
getContainingFrame(el, this.scene.getNonDeletedElementsMap()),
this.elementsPendingErasure,
null,
this.state.openDialog?.name === "elementLinkSelector" ? DEFAULT_REDUCED_GLOBAL_ALPHA : 1
),
["--embeddable-radius"]: `${getCornerRadius(
Math.min(el.width, el.height),
el
)}px`
},
children: /* @__PURE__ */ jsxs78(
"div",
{
className: "excalidraw__embeddable-container__inner",
style: {
width: isVisible ? `${el.width}px` : 0,
height: isVisible ? `${el.height}px` : 0,
transform: isVisible ? `rotate(${el.angle}rad)` : "none",
pointerEvents: isActive ? POINTER_EVENTS.enabled : POINTER_EVENTS.disabled
},
children: [
isHovered && /* @__PURE__ */ jsx147("div", { className: "excalidraw__embeddable-hint", children: t("buttons.embeddableInteractionButton") }),
/* @__PURE__ */ jsx147(
"div",
{
className: "excalidraw__embeddable__outer",
style: {
padding: `${el.strokeWidth}px`
},
children: (isEmbeddableElement(el) ? this.props.renderEmbeddable?.(el, this.state) : null) ?? /* @__PURE__ */ jsx147(
"iframe",
{
ref: (ref) => this.cacheEmbeddableRef(el, ref),
className: "excalidraw__embeddable",
srcDoc: src?.type === "document" ? src.srcdoc(this.state.theme) : void 0,
src: src?.type !== "document" ? src?.link ?? "" : void 0,
scrolling: "no",
referrerPolicy: "no-referrer-when-downgrade",
title: "Excalidraw Embedded Content",
allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
allowFullScreen: true,
sandbox: `${src?.sandbox?.allowSameOrigin ? "allow-same-origin" : ""} allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation allow-downloads`
}
)
}
)
]
}
)
},
el.id
);
}) });
}
toggleOverscrollBehavior(event) {
document.documentElement.style.overscrollBehaviorX = event.type === "pointerenter" ? "none" : "auto";
}
render() {
const selectedElements = this.scene.getSelectedElements(this.state);
const { renderTopRightUI, renderCustomStats } = this.props;
const sceneNonce = this.scene.getSceneNonce();
const { elementsMap, visibleElements } = this.renderer.getRenderableElements({
sceneNonce,
zoom: this.state.zoom,
offsetLeft: this.state.offsetLeft,
offsetTop: this.state.offsetTop,
scrollX: this.state.scrollX,
scrollY: this.state.scrollY,
height: this.state.height,
width: this.state.width,
editingTextElement: this.state.editingTextElement,
newElementId: this.state.newElement?.id,
pendingImageElementId: this.state.pendingImageElementId
});
this.visibleElements = visibleElements;
const allElementsMap = this.scene.getNonDeletedElementsMap();
const shouldBlockPointerEvents = (
// default back to `--ui-pointerEvents` flow if setPointerCapture
// not supported
"setPointerCapture" in HTMLElement.prototype ? false : this.state.selectionElement || this.state.newElement || this.state.selectedElementsAreBeingDragged || this.state.resizingElement || this.state.activeTool.type === "laser" && // technically we can just test on this once we make it more safe
this.state.cursorButton === "down"
);
const firstSelectedElement = selectedElements[0];
return /* @__PURE__ */ jsx147(
"div",
{
className: clsx55("excalidraw excalidraw-container", {
"excalidraw--view-mode": this.state.viewModeEnabled || this.state.openDialog?.name === "elementLinkSelector",
"excalidraw--mobile": this.device.editor.isMobile
}),
style: {
["--ui-pointerEvents"]: shouldBlockPointerEvents ? POINTER_EVENTS.disabled : POINTER_EVENTS.enabled,
["--right-sidebar-width"]: "302px"
},
ref: this.excalidrawContainerRef,
onDrop: this.handleAppOnDrop,
tabIndex: 0,
onKeyDown: this.props.handleKeyboardGlobally ? void 0 : this.onKeyDown,
onPointerEnter: this.toggleOverscrollBehavior,
onPointerLeave: this.toggleOverscrollBehavior,
children: /* @__PURE__ */ jsx147(AppContext.Provider, { value: this, children: /* @__PURE__ */ jsx147(AppPropsContext.Provider, { value: this.props, children: /* @__PURE__ */ jsx147(
ExcalidrawContainerContext.Provider,
{
value: this.excalidrawContainerValue,
children: /* @__PURE__ */ jsx147(DeviceContext.Provider, { value: this.device, children: /* @__PURE__ */ jsx147(ExcalidrawSetAppStateContext.Provider, { value: this.setAppState, children: /* @__PURE__ */ jsx147(ExcalidrawAppStateContext.Provider, { value: this.state, children: /* @__PURE__ */ jsxs78(
ExcalidrawElementsContext.Provider,
{
value: this.scene.getNonDeletedElements(),
children: [
/* @__PURE__ */ jsxs78(
ExcalidrawActionManagerContext.Provider,
{
value: this.actionManager,
children: [
/* @__PURE__ */ jsx147(
LayerUI_default,
{
canvas: this.canvas,
appState: this.state,
files: this.files,
setAppState: this.setAppState,
actionManager: this.actionManager,
elements: this.scene.getNonDeletedElements(),
onLockToggle: this.toggleLock,
onPenModeToggle: this.togglePenMode,
onHandToolToggle: this.onHandToolToggle,
langCode: getLanguage().code,
renderTopRightUI,
renderCustomStats,
showExitZenModeBtn: typeof this.props?.zenModeEnabled === "undefined" && this.state.zenModeEnabled,
UIOptions: this.props.UIOptions,
onExportImage: this.onExportImage,
renderWelcomeScreen: !this.state.isLoading && this.state.showWelcomeScreen && this.state.activeTool.type === "selection" && !this.state.zenModeEnabled && !this.scene.getElementsIncludingDeleted().length,
app: this,
isCollaborating: this.props.isCollaborating,
generateLinkForSelection: this.props.generateLinkForSelection,
children: this.props.children
}
),
/* @__PURE__ */ jsx147("div", { className: "excalidraw-textEditorContainer" }),
/* @__PURE__ */ jsx147("div", { className: "excalidraw-contextMenuContainer" }),
/* @__PURE__ */ jsx147("div", { className: "excalidraw-eye-dropper-container" }),
/* @__PURE__ */ jsx147(
SVGLayer,
{
trails: [this.laserTrails, this.eraserTrail]
}
),
selectedElements.length === 1 && this.state.openDialog?.name !== "elementLinkSelector" && this.state.showHyperlinkPopup && /* @__PURE__ */ jsx147(
Hyperlink,
{
element: firstSelectedElement,
elementsMap: allElementsMap,
setAppState: this.setAppState,
onLinkOpen: this.props.onLinkOpen,
setToast: this.setToast,
updateEmbedValidationStatus: this.updateEmbedValidationStatus
},
firstSelectedElement.id
),
this.props.aiEnabled !== false && selectedElements.length === 1 && isMagicFrameElement(firstSelectedElement) && /* @__PURE__ */ jsx147(
ElementCanvasButtons,
{
element: firstSelectedElement,
elementsMap,
children: /* @__PURE__ */ jsx147(
ElementCanvasButton,
{
title: t("labels.convertToCode"),
icon: MagicIcon,
checked: false,
onChange: () => this.onMagicFrameGenerate(
firstSelectedElement,
"button"
)
}
)
}
),
selectedElements.length === 1 && isIframeElement(firstSelectedElement) && firstSelectedElement.customData?.generationData?.status === "done" && /* @__PURE__ */ jsxs78(
ElementCanvasButtons,
{
element: firstSelectedElement,
elementsMap,
children: [
/* @__PURE__ */ jsx147(
ElementCanvasButton,
{
title: t("labels.copySource"),
icon: copyIcon,
checked: false,
onChange: () => this.onIframeSrcCopy(firstSelectedElement)
}
),
/* @__PURE__ */ jsx147(
ElementCanvasButton,
{
title: "Enter fullscreen",
icon: fullscreenIcon,
checked: false,
onChange: () => {
const iframe = this.getHTMLIFrameElement(
firstSelectedElement
);
if (iframe) {
try {
iframe.requestFullscreen();
this.setState({
activeEmbeddable: {
element: firstSelectedElement,
state: "active"
},
selectedElementIds: {
[firstSelectedElement.id]: true
},
newElement: null,
selectionElement: null
});
} catch (err) {
console.warn(err);
this.setState({
errorMessage: "Couldn't enter fullscreen"
});
}
}
}
}
)
]
}
),
this.state.toast !== null && /* @__PURE__ */ jsx147(
Toast,
{
message: this.state.toast.message,
onClose: () => this.setToast(null),
duration: this.state.toast.duration,
closable: this.state.toast.closable
}
),
this.state.contextMenu && /* @__PURE__ */ jsx147(
ContextMenu,
{
items: this.state.contextMenu.items,
top: this.state.contextMenu.top,
left: this.state.contextMenu.left,
actionManager: this.actionManager,
onClose: (callback) => {
this.setState({ contextMenu: null }, () => {
this.focusContainer();
callback?.();
});
}
}
),
/* @__PURE__ */ jsx147(
StaticCanvas_default,
{
canvas: this.canvas,
rc: this.rc,
elementsMap,
allElementsMap,
visibleElements,
sceneNonce,
selectionNonce: this.state.selectionElement?.versionNonce,
scale: window.devicePixelRatio,
appState: this.state,
renderConfig: {
imageCache: this.imageCache,
isExporting: false,
renderGrid: isGridModeEnabled(this),
canvasBackgroundColor: this.state.viewBackgroundColor,
embedsValidationStatus: this.embedsValidationStatus,
elementsPendingErasure: this.elementsPendingErasure,
pendingFlowchartNodes: this.flowChartCreator.pendingNodes
}
}
),
this.state.newElement && /* @__PURE__ */ jsx147(
NewElementCanvas_default,
{
appState: this.state,
scale: window.devicePixelRatio,
rc: this.rc,
elementsMap,
allElementsMap,
renderConfig: {
imageCache: this.imageCache,
isExporting: false,
renderGrid: false,
canvasBackgroundColor: this.state.viewBackgroundColor,
embedsValidationStatus: this.embedsValidationStatus,
elementsPendingErasure: this.elementsPendingErasure,
pendingFlowchartNodes: null
}
}
),
/* @__PURE__ */ jsx147(
InteractiveCanvas_default,
{
containerRef: this.excalidrawContainerRef,
canvas: this.interactiveCanvas,
elementsMap,
visibleElements,
allElementsMap,
selectedElements,
sceneNonce,
selectionNonce: this.state.selectionElement?.versionNonce,
scale: window.devicePixelRatio,
appState: this.state,
device: this.device,
renderInteractiveSceneCallback: this.renderInteractiveSceneCallback,
handleCanvasRef: this.handleInteractiveCanvasRef,
onContextMenu: this.handleCanvasContextMenu,
onPointerMove: this.handleCanvasPointerMove,
onPointerUp: this.handleCanvasPointerUp,
onPointerCancel: this.removePointer,
onTouchMove: this.handleTouchMove,
onPointerDown: this.handleCanvasPointerDown,
onDoubleClick: this.handleCanvasDoubleClick
}
),
this.state.userToFollow && /* @__PURE__ */ jsx147(
FollowMode_default,
{
width: this.state.width,
height: this.state.height,
userToFollow: this.state.userToFollow,
onDisconnect: this.maybeUnfollowRemoteUser
}
),
this.renderFrameNames()
]
}
),
this.renderEmbeddables()
]
}
) }) }) })
}
) }) })
}
);
}
setPlugins(plugins) {
Object.assign(this.plugins, plugins);
}
async onMagicFrameGenerate(magicFrame, source) {
const generateDiagramToCode = this.plugins.diagramToCode?.generate;
if (!generateDiagramToCode) {
this.setState({
errorMessage: "No diagram to code plugin found"
});
return;
}
const magicFrameChildren = getElementsOverlappingFrame(
this.scene.getNonDeletedElements(),
magicFrame
).filter((el) => !isMagicFrameElement(el));
if (!magicFrameChildren.length) {
if (source === "button") {
this.setState({ errorMessage: "Cannot generate from an empty frame" });
trackEvent("ai", "generate (no-children)", "d2c");
} else {
this.setActiveTool({ type: "magicframe" });
}
return;
}
const frameElement = this.insertIframeElement({
sceneX: magicFrame.x + magicFrame.width + 30,
sceneY: magicFrame.y,
width: magicFrame.width,
height: magicFrame.height
});
if (!frameElement) {
return;
}
this.updateMagicGeneration({
frameElement,
data: { status: "pending" }
});
this.setState({
selectedElementIds: { [frameElement.id]: true }
});
trackEvent("ai", "generate (start)", "d2c");
try {
const { html } = await generateDiagramToCode({
frame: magicFrame,
children: magicFrameChildren
});
trackEvent("ai", "generate (success)", "d2c");
if (!html.trim()) {
this.updateMagicGeneration({
frameElement,
data: {
status: "error",
code: "ERR_OAI",
message: "Nothing genereated :("
}
});
return;
}
const parsedHtml = html.includes("<!DOCTYPE html>") && html.includes("</html>") ? html.slice(
html.indexOf("<!DOCTYPE html>"),
html.indexOf("</html>") + "</html>".length
) : html;
this.updateMagicGeneration({
frameElement,
data: { status: "done", html: parsedHtml }
});
} catch (error) {
trackEvent("ai", "generate (failed)", "d2c");
this.updateMagicGeneration({
frameElement,
data: {
status: "error",
code: "ERR_OAI",
message: error.message || "Unknown error during generation"
}
});
}
}
onIframeSrcCopy(element) {
if (element.customData?.generationData?.status === "done") {
copyTextToSystemClipboard(element.customData.generationData.html);
this.setToast({
message: "copied to clipboard",
closable: false,
duration: 1500
});
}
}
clearImageShapeCache(filesMap) {
const files = filesMap ?? this.files;
this.scene.getNonDeletedElements().forEach((element) => {
if (isInitializedImageElement(element) && files[element.fileId]) {
this.imageCache.delete(element.fileId);
ShapeCache.delete(element);
}
});
}
async componentDidMount() {
this.unmounted = false;
this.excalidrawContainerValue.container = this.excalidrawContainerRef.current;
if (define_import_meta_env_default.MODE === ENV.TEST || define_import_meta_env_default.DEV) {
const setState = this.setState.bind(this);
Object.defineProperties(window.h, {
state: {
configurable: true,
get: () => {
return this.state;
}
},
setState: {
configurable: true,
value: (...args) => {
return this.setState(...args);
}
},
app: {
configurable: true,
value: this
},
history: {
configurable: true,
value: this.history
},
store: {
configurable: true,
value: this.store
},
fonts: {
configurable: true,
value: this.fonts
}
});
}
this.store.onStoreIncrementEmitter.on((increment) => {
this.history.record(increment.elementsChange, increment.appStateChange);
});
this.scene.onUpdate(this.triggerRender);
this.addEventListeners();
if (this.props.autoFocus && this.excalidrawContainerRef.current) {
this.focusContainer();
}
if (
// bounding rects don't work in tests so updating
// the state on init would result in making the test enviro run
// in mobile breakpoint (0 width/height), making everything fail
!isTestEnv()
) {
this.refreshViewportBreakpoints();
this.refreshEditorBreakpoints();
}
if (supportsResizeObserver && this.excalidrawContainerRef.current) {
this.resizeObserver = new ResizeObserver(() => {
this.refreshEditorBreakpoints();
this.updateDOMRect();
});
this.resizeObserver?.observe(this.excalidrawContainerRef.current);
}
const searchParams = new URLSearchParams(window.location.search.slice(1));
if (searchParams.has("web-share-target")) {
this.restoreFileFromShare();
} else {
this.updateDOMRect(this.initializeScene);
}
if (isBrave() && !isMeasureTextSupported()) {
this.setState({
errorMessage: /* @__PURE__ */ jsx147(BraveMeasureTextError_default, {})
});
}
}
componentWillUnmount() {
window.launchQueue?.setConsumer(() => {
});
this.renderer.destroy();
this.scene.destroy();
this.scene = new Scene_default();
this.fonts = new Fonts(this.scene);
this.renderer = new Renderer(this.scene);
this.files = {};
this.imageCache.clear();
this.resizeObserver?.disconnect();
this.unmounted = true;
this.removeEventListeners();
this.library.destroy();
this.laserTrails.stop();
this.eraserTrail.stop();
this.onChangeEmitter.clear();
this.store.onStoreIncrementEmitter.clear();
ShapeCache.destroy();
SnapCache.destroy();
clearTimeout(touchTimeout);
isSomeElementSelected.clearCache();
selectGroupsForSelectedElements.clearCache();
touchTimeout = 0;
document.documentElement.style.overscrollBehaviorX = "";
}
removeEventListeners() {
this.onRemoveEventListenersEmitter.trigger();
}
addEventListeners() {
this.removeEventListeners();
if (this.props.handleKeyboardGlobally) {
this.onRemoveEventListenersEmitter.once(
addEventListener(document, "keydown" /* KEYDOWN */, this.onKeyDown, false)
);
}
this.onRemoveEventListenersEmitter.once(
addEventListener(
this.excalidrawContainerRef.current,
"wheel" /* WHEEL */,
this.handleWheel,
{ passive: false }
),
addEventListener(window, "message" /* MESSAGE */, this.onWindowMessage, false),
addEventListener(document, "pointerup" /* POINTER_UP */, this.removePointer, {
passive: false
}),
// #3553
addEventListener(document, "copy" /* COPY */, this.onCopy, { passive: false }),
addEventListener(document, "keyup" /* KEYUP */, this.onKeyUp, { passive: true }),
addEventListener(
document,
"pointermove" /* POINTER_MOVE */,
this.updateCurrentCursorPosition,
{ passive: false }
),
// rerender text elements on font load to fix #637 && #1553
addEventListener(
document.fonts,
"loadingdone",
(event) => {
const fontFaces = event.fontfaces;
this.fonts.onLoaded(fontFaces);
},
{ passive: false }
),
// Safari-only desktop pinch zoom
addEventListener(
document,
"gesturestart" /* GESTURE_START */,
this.onGestureStart,
false
),
addEventListener(
document,
"gesturechange" /* GESTURE_CHANGE */,
this.onGestureChange,
false
),
addEventListener(
document,
"gestureend" /* GESTURE_END */,
this.onGestureEnd,
false
),
addEventListener(
window,
"focus" /* FOCUS */,
() => {
this.maybeCleanupAfterMissingPointerUp(null);
this.triggerRender(true);
},
{ passive: false }
)
);
if (this.state.viewModeEnabled) {
return;
}
this.onRemoveEventListenersEmitter.once(
addEventListener(
document,
"fullscreenchange" /* FULLSCREENCHANGE */,
this.onFullscreenChange,
{ passive: false }
),
addEventListener(document, "paste" /* PASTE */, this.pasteFromClipboard, {
passive: false
}),
addEventListener(document, "cut" /* CUT */, this.onCut, { passive: false }),
addEventListener(window, "resize" /* RESIZE */, this.onResize, false),
addEventListener(window, "unload" /* UNLOAD */, this.onUnload, false),
addEventListener(window, "blur" /* BLUR */, this.onBlur, false),
addEventListener(
this.excalidrawContainerRef.current,
"wheel" /* WHEEL */,
this.handleWheel,
{ passive: false }
),
addEventListener(
this.excalidrawContainerRef.current,
"dragover" /* DRAG_OVER */,
this.disableEvent,
false
),
addEventListener(
this.excalidrawContainerRef.current,
"drop" /* DROP */,
this.disableEvent,
false
)
);
if (this.props.detectScroll) {
this.onRemoveEventListenersEmitter.once(
addEventListener(
getNearestScrollableContainer(this.excalidrawContainerRef.current),
"scroll" /* SCROLL */,
this.onScroll,
{ passive: false }
)
);
}
}
componentDidUpdate(prevProps, prevState) {
this.updateEmbeddables();
const elements = this.scene.getElementsIncludingDeleted();
const elementsMap = this.scene.getElementsMapIncludingDeleted();
const nonDeletedElementsMap = this.scene.getNonDeletedElementsMap();
if (!this.state.showWelcomeScreen && !elements.length) {
this.setState({ showWelcomeScreen: true });
}
if (prevProps.UIOptions.dockedSidebarBreakpoint !== this.props.UIOptions.dockedSidebarBreakpoint) {
this.refreshEditorBreakpoints();
}
const hasFollowedPersonLeft = prevState.userToFollow && !this.state.collaborators.has(prevState.userToFollow.socketId);
if (hasFollowedPersonLeft) {
this.maybeUnfollowRemoteUser();
}
if (prevState.zoom.value !== this.state.zoom.value || prevState.scrollX !== this.state.scrollX || prevState.scrollY !== this.state.scrollY) {
this.props?.onScrollChange?.(
this.state.scrollX,
this.state.scrollY,
this.state.zoom
);
this.onScrollChangeEmitter.trigger(
this.state.scrollX,
this.state.scrollY,
this.state.zoom
);
}
if (prevState.userToFollow !== this.state.userToFollow) {
if (prevState.userToFollow) {
this.onUserFollowEmitter.trigger({
userToFollow: prevState.userToFollow,
action: "UNFOLLOW"
});
}
if (this.state.userToFollow) {
this.onUserFollowEmitter.trigger({
userToFollow: this.state.userToFollow,
action: "FOLLOW"
});
}
}
if (Object.keys(this.state.selectedElementIds).length && isEraserActive(this.state)) {
this.setState({
activeTool: updateActiveTool(this.state, { type: "selection" })
});
}
if (this.state.activeTool.type === "eraser" && prevState.theme !== this.state.theme) {
setEraserCursor(this.interactiveCanvas, this.state.theme);
}
if (prevState.activeTool.type === "selection" && this.state.activeTool.type !== "selection" && this.state.showHyperlinkPopup) {
this.setState({ showHyperlinkPopup: false });
}
if (prevProps.langCode !== this.props.langCode) {
this.updateLanguage();
}
if (isEraserActive(prevState) && !isEraserActive(this.state)) {
this.eraserTrail.endPath();
}
if (prevProps.viewModeEnabled !== this.props.viewModeEnabled) {
this.setState({ viewModeEnabled: !!this.props.viewModeEnabled });
}
if (prevState.viewModeEnabled !== this.state.viewModeEnabled) {
this.addEventListeners();
this.deselectElements();
}
if ((prevState.openDialog?.name === "elementLinkSelector" || this.state.openDialog?.name === "elementLinkSelector") && prevState.openDialog?.name !== this.state.openDialog?.name) {
this.deselectElements();
this.setState({
hoveredElementIds: {}
});
}
if (prevProps.zenModeEnabled !== this.props.zenModeEnabled) {
this.setState({ zenModeEnabled: !!this.props.zenModeEnabled });
}
if (prevProps.theme !== this.props.theme && this.props.theme) {
this.setState({ theme: this.props.theme });
}
this.excalidrawContainerRef.current?.classList.toggle(
"theme--dark",
this.state.theme === THEME.DARK
);
if (this.state.editingLinearElement && !this.state.selectedElementIds[this.state.editingLinearElement.elementId]) {
setTimeout(() => {
this.state.editingLinearElement && this.actionManager.executeAction(actionFinalize);
});
}
if (this.state.editingTextElement?.isDeleted) {
this.setState({ editingTextElement: null });
}
if (this.state.selectedLinearElement && !this.state.selectedElementIds[this.state.selectedLinearElement.elementId]) {
this.setState({ selectedLinearElement: null });
}
const { multiElement } = prevState;
if (prevState.activeTool !== this.state.activeTool && multiElement != null && isBindingEnabled(this.state) && isBindingElement(multiElement, false)) {
maybeBindLinearElement(
multiElement,
this.state,
tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(
multiElement,
-1,
nonDeletedElementsMap
)
),
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements()
);
}
this.store.commit(elementsMap, this.state);
if (!this.state.isLoading) {
this.props.onChange?.(elements, this.state, this.files);
this.onChangeEmitter.trigger(elements, this.state, this.files);
}
}
static resetTapTwice() {
didTapTwice = false;
}
// TODO rewrite this to paste both text & images at the same time if
// pasted data contains both
async addElementsFromMixedContentPaste(mixedContent, {
isPlainPaste,
sceneX,
sceneY
}) {
if (!isPlainPaste && mixedContent.some((node) => node.type === "imageUrl") && this.isToolSupported("image")) {
const imageURLs = mixedContent.filter((node) => node.type === "imageUrl").map((node) => node.value);
const responses = await Promise.all(
imageURLs.map(async (url) => {
try {
return { file: await ImageURLToFile(url) };
} catch (error2) {
let errorMessage = error2.message;
if (error2.cause === "FETCH_ERROR") {
errorMessage = t("errors.failedToFetchImage");
} else if (error2.cause === "UNSUPPORTED") {
errorMessage = t("errors.unsupportedFileType");
}
return { errorMessage };
}
})
);
let y = sceneY;
let firstImageYOffsetDone = false;
const nextSelectedIds = {};
for (const response of responses) {
if (response.file) {
const imageElement = this.createImageElement({
sceneX,
sceneY: y
});
const initializedImageElement = await this.insertImageElement(
imageElement,
response.file
);
if (initializedImageElement) {
if (!firstImageYOffsetDone) {
firstImageYOffsetDone = true;
y -= initializedImageElement.height / 2;
}
mutateElement(initializedImageElement, { y }, false);
y = imageElement.y + imageElement.height + 25;
nextSelectedIds[imageElement.id] = true;
}
}
}
this.setState({
selectedElementIds: makeNextSelectedElementIds(
nextSelectedIds,
this.state
)
});
const error = responses.find((response) => !!response.errorMessage);
if (error && error.errorMessage) {
this.setState({ errorMessage: error.errorMessage });
}
} else {
const textNodes = mixedContent.filter((node) => node.type === "text");
if (textNodes.length) {
this.addTextFromPaste(
textNodes.map((node) => node.value).join("\n\n"),
isPlainPaste
);
}
}
}
addTextFromPaste(text, isPlainPaste = false) {
const { x, y } = viewportCoordsToSceneCoords(
{
clientX: this.lastViewportPosition.x,
clientY: this.lastViewportPosition.y
},
this.state
);
const textElementProps = {
x,
y,
strokeColor: this.state.currentItemStrokeColor,
backgroundColor: this.state.currentItemBackgroundColor,
fillStyle: this.state.currentItemFillStyle,
strokeWidth: this.state.currentItemStrokeWidth,
strokeStyle: this.state.currentItemStrokeStyle,
roundness: null,
roughness: this.state.currentItemRoughness,
opacity: this.state.currentItemOpacity,
text,
fontSize: this.state.currentItemFontSize,
fontFamily: this.state.currentItemFontFamily,
textAlign: DEFAULT_TEXT_ALIGN,
verticalAlign: DEFAULT_VERTICAL_ALIGN,
locked: false
};
const fontString = getFontString({
fontSize: textElementProps.fontSize,
fontFamily: textElementProps.fontFamily
});
const lineHeight = getLineHeight(textElementProps.fontFamily);
const [x1, , x2] = getVisibleSceneBounds(this.state);
const maxTextWidth = Math.max(Math.min((x2 - x1) * 0.5, 800), 200);
const LINE_GAP = 10;
let currentY = y;
const lines = isPlainPaste ? [text] : text.split("\n");
const textElements = lines.reduce(
(acc, line, idx) => {
const originalText = normalizeText(line).trim();
if (originalText.length) {
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({
x,
y: currentY
});
let metrics = measureText(originalText, fontString, lineHeight);
const isTextUnwrapped = metrics.width > maxTextWidth;
const text2 = isTextUnwrapped ? wrapText(originalText, fontString, maxTextWidth) : originalText;
metrics = isTextUnwrapped ? measureText(text2, fontString, lineHeight) : metrics;
const startX = x - metrics.width / 2;
const startY = currentY - metrics.height / 2;
const element = newTextElement({
...textElementProps,
x: startX,
y: startY,
text: text2,
originalText,
lineHeight,
autoResize: !isTextUnwrapped,
frameId: topLayerFrame ? topLayerFrame.id : null
});
acc.push(element);
currentY += element.height + LINE_GAP;
} else {
const prevLine = lines[idx - 1]?.trim();
if (prevLine) {
currentY += getLineHeightInPx(textElementProps.fontSize, lineHeight) + LINE_GAP;
}
}
return acc;
},
[]
);
if (textElements.length === 0) {
return;
}
this.scene.insertElements(textElements);
this.setState({
selectedElementIds: makeNextSelectedElementIds(
Object.fromEntries(textElements.map((el) => [el.id, true])),
this.state
)
});
if (!isPlainPaste && textElements.length > 1 && PLAIN_PASTE_TOAST_SHOWN === false && !this.device.editor.isMobile) {
this.setToast({
message: t("toast.pasteAsSingleElement", {
shortcut: getShortcutKey("CtrlOrCmd+Shift+V")
}),
duration: 5e3
});
PLAIN_PASTE_TOAST_SHOWN = true;
}
this.store.shouldCaptureIncrement();
}
handleTextWysiwyg(element, {
isExistingElement = false
}) {
const elementsMap = this.scene.getElementsMapIncludingDeleted();
const updateElement = (nextOriginalText, isDeleted) => {
this.scene.replaceAllElements([
// Not sure why we include deleted elements as well hence using deleted elements map
...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) {
return newElementWith(_element, {
originalText: nextOriginalText,
isDeleted: isDeleted ?? _element.isDeleted,
// returns (wrapped) text and new dimensions
...refreshTextDimensions(
_element,
getContainerElement(_element, elementsMap),
elementsMap,
nextOriginalText
)
});
}
return _element;
})
]);
};
textWysiwyg({
id: element.id,
canvas: this.canvas,
getViewportCoords: (x, y) => {
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{
sceneX: x,
sceneY: y
},
this.state
);
return [
viewportX - this.state.offsetLeft,
viewportY - this.state.offsetTop
];
},
onChange: withBatchedUpdates((nextOriginalText) => {
updateElement(nextOriginalText, false);
if (isNonDeletedElement(element)) {
updateBoundElements(element, this.scene.getNonDeletedElementsMap());
}
}),
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
const isDeleted = !nextOriginalText.trim();
updateElement(nextOriginalText, isDeleted);
if (!isDeleted && viaKeyboard) {
const elementIdToSelect = element.containerId ? element.containerId : element.id;
flushSync2(() => {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[elementIdToSelect]: true
},
prevState
)
}));
});
}
if (isDeleted) {
fixBindingsAfterDeletion(this.scene.getNonDeletedElements(), [
element
]);
}
if (!isDeleted || isExistingElement) {
this.store.shouldCaptureIncrement();
}
flushSync2(() => {
this.setState({
newElement: null,
editingTextElement: null
});
});
if (this.state.activeTool.locked) {
setCursorForShape(this.interactiveCanvas, this.state);
}
this.focusContainer();
}),
element,
excalidrawContainer: this.excalidrawContainerRef.current,
app: this,
// when text is selected, it's hard (at least on iOS) to re-position the
// caret (i.e. deselect). There's not much use for always selecting
// the text on edit anyway (and users can select-all from contextmenu
// if needed)
autoSelect: !this.device.isTouchScreen
});
this.deselectElements();
updateElement(element.originalText, false);
}
deselectElements() {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedGroupIds: {},
editingGroupId: null,
activeEmbeddable: null
});
}
getTextElementAtPosition(x, y) {
const element = this.getElementAtPosition(x, y, {
includeBoundTextElement: true
});
if (element && isTextElement(element) && !element.isDeleted) {
return element;
}
return null;
}
getElementAtPosition(x, y, opts) {
const allHitElements = this.getElementsAtPosition(
x,
y,
opts?.includeBoundTextElement,
opts?.includeLockedElements
);
if (allHitElements.length > 1) {
if (opts?.preferSelected) {
for (let index = allHitElements.length - 1; index > -1; index--) {
if (this.state.selectedElementIds[allHitElements[index].id]) {
return allHitElements[index];
}
}
}
const elementWithHighestZIndex = allHitElements[allHitElements.length - 1];
return hitElementItself({
x,
y,
element: elementWithHighestZIndex,
shape: getElementShape(
elementWithHighestZIndex,
this.scene.getNonDeletedElementsMap()
),
// when overlapping, we would like to be more precise
// this also avoids the need to update past tests
threshold: this.getElementHitThreshold() / 2,
frameNameBound: isFrameLikeElement(elementWithHighestZIndex) ? this.frameNameBoundsCache.get(elementWithHighestZIndex) : null
}) ? elementWithHighestZIndex : allHitElements[allHitElements.length - 2];
}
if (allHitElements.length === 1) {
return allHitElements[0];
}
return null;
}
getElementsAtPosition(x, y, includeBoundTextElement = false, includeLockedElements = false) {
const iframeLikes = [];
const elementsMap = this.scene.getNonDeletedElementsMap();
const elements = (includeBoundTextElement && includeLockedElements ? this.scene.getNonDeletedElements() : this.scene.getNonDeletedElements().filter(
(element) => (includeLockedElements || !element.locked) && (includeBoundTextElement || !(isTextElement(element) && element.containerId))
)).filter((el) => this.hitElement(x, y, el)).filter((element) => {
const containingFrame = getContainingFrame(element, elementsMap);
return containingFrame && this.state.frameRendering.enabled && this.state.frameRendering.clip ? isCursorInFrame({ x, y }, containingFrame, elementsMap) : true;
}).filter((el) => {
if (isIframeElement(el)) {
iframeLikes.push(el);
return false;
}
return true;
}).concat(iframeLikes);
return elements;
}
getElementHitThreshold() {
return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value;
}
hitElement(x, y, element, considerBoundingBox = true) {
if (considerBoundingBox && this.state.selectedElementIds[element.id] && shouldShowBoundingBox([element], this.state)) {
const selectionShape = getSelectionBoxShape(
element,
this.scene.getNonDeletedElementsMap(),
isImageElement(element) ? 0 : this.getElementHitThreshold()
);
if (isPointInShape(pointFrom(x, y), selectionShape)) {
return true;
}
}
const hitBoundTextOfElement = hitElementBoundText(
x,
y,
getBoundTextShape(element, this.scene.getNonDeletedElementsMap())
);
if (hitBoundTextOfElement) {
return true;
}
return hitElementItself({
x,
y,
element,
shape: getElementShape(element, this.scene.getNonDeletedElementsMap()),
threshold: this.getElementHitThreshold(),
frameNameBound: isFrameLikeElement(element) ? this.frameNameBoundsCache.get(element) : null
});
}
getTextBindableContainerAtPosition(x, y) {
const elements = this.scene.getNonDeletedElements();
const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1) {
return isTextBindableContainer(selectedElements[0], false) ? selectedElements[0] : null;
}
let hitElement = null;
for (let index = elements.length - 1; index >= 0; --index) {
if (elements[index].isDeleted) {
continue;
}
const [x1, y1, x2, y2] = getElementAbsoluteCoords(
elements[index],
this.scene.getNonDeletedElementsMap()
);
if (isArrowElement(elements[index]) && hitElementItself({
x,
y,
element: elements[index],
shape: getElementShape(
elements[index],
this.scene.getNonDeletedElementsMap()
),
threshold: this.getElementHitThreshold()
})) {
hitElement = elements[index];
break;
} else if (x1 < x && x < x2 && y1 < y && y < y2) {
hitElement = elements[index];
break;
}
}
return isTextBindableContainer(hitElement, false) ? hitElement : null;
}
handleHoverSelectedLinearElement(linearElementEditor, scenePointerX, scenePointerY) {
const elementsMap = this.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(
linearElementEditor.elementId,
elementsMap
);
if (!element) {
return;
}
if (this.state.selectedLinearElement) {
let hoverPointIndex = -1;
let segmentMidPointHoveredCoords = null;
if (hitElementItself({
x: scenePointerX,
y: scenePointerY,
element,
shape: getElementShape(
element,
this.scene.getNonDeletedElementsMap()
)
})) {
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
element,
elementsMap,
this.state.zoom,
scenePointerX,
scenePointerY
);
segmentMidPointHoveredCoords = LinearElementEditor.getSegmentMidpointHitCoords(
linearElementEditor,
{ x: scenePointerX, y: scenePointerY },
this.state,
this.scene.getNonDeletedElementsMap()
);
const isHoveringAPointHandle = isElbowArrow(element) ? hoverPointIndex === 0 || hoverPointIndex === element.points.length - 1 : hoverPointIndex >= 0;
if (isHoveringAPointHandle || segmentMidPointHoveredCoords) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
}
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
if (
// Ebow arrows can only be moved when unconnected
!isElbowArrow(element) || !(element.startBinding || element.endBinding)
) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
}
}
if (this.state.selectedLinearElement.hoverPointIndex !== hoverPointIndex) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
hoverPointIndex
}
});
}
if (!LinearElementEditor.arePointsEqual(
this.state.selectedLinearElement.segmentMidPointHoveredCoords,
segmentMidPointHoveredCoords
)) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
segmentMidPointHoveredCoords
}
});
}
} else {
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
}
}
updateGestureOnPointerDown(event) {
gesture.pointers.set(event.pointerId, {
x: event.clientX,
y: event.clientY
});
if (gesture.pointers.size === 2) {
gesture.lastCenter = getCenter(gesture.pointers);
gesture.initialScale = this.state.zoom.value;
gesture.initialDistance = getDistance(
Array.from(gesture.pointers.values())
);
}
}
initialPointerDownState(event) {
const origin = viewportCoordsToSceneCoords(event, this.state);
const selectedElements = this.scene.getSelectedElements(this.state);
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
const isElbowArrowOnly = selectedElements.findIndex(isElbowArrow) === 0;
return {
origin,
withCmdOrCtrl: event[KEYS.CTRL_OR_CMD],
originInGrid: tupleToCoors(
getGridPoint(
origin.x,
origin.y,
event[KEYS.CTRL_OR_CMD] || isElbowArrowOnly ? null : this.getEffectiveGridSize()
)
),
scrollbars: isOverScrollBars(
currentScrollBars,
event.clientX - this.state.offsetLeft,
event.clientY - this.state.offsetTop
),
// we need to duplicate because we'll be updating this state
lastCoords: { ...origin },
originalElements: this.scene.getNonDeletedElements().reduce((acc, element) => {
acc.set(element.id, deepCopyElement(element));
return acc;
}, /* @__PURE__ */ new Map()),
resize: {
handleType: false,
isResizing: false,
offset: { x: 0, y: 0 },
arrowDirection: "origin",
center: { x: (maxX + minX) / 2, y: (maxY + minY) / 2 }
},
hit: {
element: null,
allHitElements: [],
wasAddedToSelection: false,
hasBeenDuplicated: false,
hasHitCommonBoundingBoxOfSelectedElements: this.isHittingCommonBoundingBoxOfSelectedElements(
origin,
selectedElements
)
},
drag: {
hasOccurred: false,
offset: null
},
eventListeners: {
onMove: null,
onUp: null,
onKeyUp: null,
onKeyDown: null
},
boxSelection: {
hasOccurred: false
}
};
}
// Returns whether the event is a dragging a scrollbar
handleDraggingScrollBar(event, pointerDownState) {
if (!(pointerDownState.scrollbars.isOverEither && !this.state.multiElement)) {
return false;
}
isDraggingScrollBar = true;
pointerDownState.lastCoords.x = event.clientX;
pointerDownState.lastCoords.y = event.clientY;
const onPointerMove = withBatchedUpdatesThrottled((event2) => {
const target = event2.target;
if (!(target instanceof HTMLElement)) {
return;
}
this.handlePointerMoveOverScrollbars(event2, pointerDownState);
});
const onPointerUp = withBatchedUpdates(() => {
lastPointerUp = null;
isDraggingScrollBar = false;
setCursorForShape(this.interactiveCanvas, this.state);
this.setState({
cursorButton: "up"
});
this.savePointer(event.clientX, event.clientY, "up");
window.removeEventListener("pointermove" /* POINTER_MOVE */, onPointerMove);
window.removeEventListener("pointerup" /* POINTER_UP */, onPointerUp);
onPointerMove.flush();
});
lastPointerUp = onPointerUp;
window.addEventListener("pointermove" /* POINTER_MOVE */, onPointerMove);
window.addEventListener("pointerup" /* POINTER_UP */, onPointerUp);
return true;
}
isASelectedElement(hitElement) {
return hitElement != null && this.state.selectedElementIds[hitElement.id];
}
isHittingCommonBoundingBoxOfSelectedElements(point, selectedElements) {
if (selectedElements.length < 2) {
return false;
}
const threshold = this.getElementHitThreshold();
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
return point.x > x1 - threshold && point.x < x2 + threshold && point.y > y1 - threshold && point.y < y2 + threshold;
}
getCurrentItemRoundness(elementType) {
return this.state.currentItemRoundness === "round" ? {
type: isUsingAdaptiveRadius(elementType) ? ROUNDNESS.ADAPTIVE_RADIUS : ROUNDNESS.PROPORTIONAL_RADIUS
} : null;
}
maybeCacheReferenceSnapPoints(event, selectedElements, recomputeAnyways = false) {
if (isSnappingEnabled({
event,
app: this,
selectedElements
}) && (recomputeAnyways || !SnapCache.getReferenceSnapPoints())) {
SnapCache.setReferenceSnapPoints(
getReferenceSnapPoints(
this.scene.getNonDeletedElements(),
selectedElements,
this.state,
this.scene.getNonDeletedElementsMap()
)
);
}
}
maybeCacheVisibleGaps(event, selectedElements, recomputeAnyways = false) {
if (isSnappingEnabled({
event,
app: this,
selectedElements
}) && (recomputeAnyways || !SnapCache.getVisibleGaps())) {
SnapCache.setVisibleGaps(
getVisibleGaps(
this.scene.getNonDeletedElements(),
selectedElements,
this.state,
this.scene.getNonDeletedElementsMap()
)
);
}
}
onKeyDownFromPointerDownHandler(pointerDownState) {
return withBatchedUpdates((event) => {
if (this.maybeHandleResize(pointerDownState, event)) {
return;
}
this.maybeDragNewGenericElement(pointerDownState, event);
});
}
onKeyUpFromPointerDownHandler(pointerDownState) {
return withBatchedUpdates((event) => {
event.key === KEYS.ALT && event.preventDefault();
if (this.maybeHandleResize(pointerDownState, event)) {
return;
}
this.maybeDragNewGenericElement(pointerDownState, event);
});
}
onPointerMoveFromPointerDownHandler(pointerDownState) {
return withBatchedUpdatesThrottled((event) => {
if (this.state.openDialog?.name === "elementLinkSelector") {
return;
}
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
if (this.state.selectedLinearElement && this.state.selectedLinearElement.elbowed && this.state.selectedLinearElement.pointerDownState.segmentMidpoint.index) {
const [gridX2, gridY2] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
let index = this.state.selectedLinearElement.pointerDownState.segmentMidpoint.index;
if (index < 0) {
const nextCoords = LinearElementEditor.getSegmentMidpointHitCoords(
{
...this.state.selectedLinearElement,
segmentMidPointHoveredCoords: null
},
{ x: gridX2, y: gridY2 },
this.state,
this.scene.getNonDeletedElementsMap()
);
index = nextCoords ? LinearElementEditor.getSegmentMidPointIndex(
this.state.selectedLinearElement,
this.state,
nextCoords,
this.scene.getNonDeletedElementsMap()
) : -1;
}
const ret = LinearElementEditor.moveFixedSegment(
this.state.selectedLinearElement,
index,
gridX2,
gridY2,
this.scene.getNonDeletedElementsMap()
);
flushSync2(() => {
if (this.state.selectedLinearElement) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
segmentMidPointHoveredCoords: ret.segmentMidPointHoveredCoords,
pointerDownState: ret.pointerDownState
}
});
}
});
return;
}
const lastPointerCoords = this.lastPointerMoveCoords ?? pointerDownState.origin;
this.lastPointerMoveCoords = pointerCoords;
if (pointerDownState.drag.offset === null) {
pointerDownState.drag.offset = tupleToCoors(
getDragOffsetXY(
this.scene.getSelectedElements(this.state),
pointerDownState.origin.x,
pointerDownState.origin.y
)
);
}
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
if (this.handlePointerMoveOverScrollbars(event, pointerDownState)) {
return;
}
if (isEraserActive(this.state)) {
this.handleEraser(event, pointerDownState, pointerCoords);
return;
}
if (this.state.activeTool.type === "laser") {
this.laserTrails.addPointToPath(pointerCoords.x, pointerCoords.y);
}
const [gridX, gridY] = getGridPoint(
pointerCoords.x,
pointerCoords.y,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
if (!pointerDownState.drag.hasOccurred && (this.state.activeTool.type === "arrow" || this.state.activeTool.type === "line")) {
if (pointDistance(
pointFrom(pointerCoords.x, pointerCoords.y),
pointFrom(pointerDownState.origin.x, pointerDownState.origin.y)
) < DRAGGING_THRESHOLD) {
return;
}
}
if (pointerDownState.resize.isResizing) {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
if (this.maybeHandleCrop(pointerDownState, event)) {
return true;
}
if (this.maybeHandleResize(pointerDownState, event)) {
return true;
}
}
const elementsMap = this.scene.getNonDeletedElementsMap();
if (this.state.selectedLinearElement) {
const linearElementEditor = this.state.editingLinearElement || this.state.selectedLinearElement;
if (LinearElementEditor.shouldAddMidpoint(
this.state.selectedLinearElement,
pointerCoords,
this.state,
elementsMap
)) {
const ret = LinearElementEditor.addMidpoint(
this.state.selectedLinearElement,
pointerCoords,
this,
!event[KEYS.CTRL_OR_CMD],
elementsMap
);
if (!ret) {
return;
}
flushSync2(() => {
if (this.state.selectedLinearElement) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
pointerDownState: ret.pointerDownState,
selectedPointsIndices: ret.selectedPointsIndices
}
});
}
if (this.state.editingLinearElement) {
this.setState({
editingLinearElement: {
...this.state.editingLinearElement,
pointerDownState: ret.pointerDownState,
selectedPointsIndices: ret.selectedPointsIndices
}
});
}
});
return;
} else if (linearElementEditor.pointerDownState.segmentMidpoint.value !== null && !linearElementEditor.pointerDownState.segmentMidpoint.added) {
return;
}
const didDrag = LinearElementEditor.handlePointDragging(
event,
this,
pointerCoords.x,
pointerCoords.y,
(element, pointsSceneCoords) => {
this.maybeSuggestBindingsForLinearElementAtCoords(
element,
pointsSceneCoords
);
},
linearElementEditor,
this.scene
);
if (didDrag) {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
pointerDownState.drag.hasOccurred = true;
if (this.state.editingLinearElement && !this.state.editingLinearElement.isDragging) {
this.setState({
editingLinearElement: {
...this.state.editingLinearElement,
isDragging: true
}
});
}
if (!this.state.selectedLinearElement.isDragging) {
this.setState({
selectedLinearElement: {
...this.state.selectedLinearElement,
isDragging: true
}
});
}
return;
}
}
const hasHitASelectedElement = pointerDownState.hit.allHitElements.some(
(element) => this.isASelectedElement(element)
);
const isSelectingPointsInLineEditor = this.state.editingLinearElement && event.shiftKey && this.state.editingLinearElement.elementId === pointerDownState.hit.element?.id;
if ((hasHitASelectedElement || pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) && !isSelectingPointsInLineEditor) {
const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.every((element) => element.locked)) {
return;
}
const selectedElementsHasAFrame = selectedElements.find(
(e) => isFrameLikeElement(e)
);
const topLayerFrame = this.getTopLayerFrameAtSceneCoords(pointerCoords);
const frameToHighlight = topLayerFrame && !selectedElementsHasAFrame ? topLayerFrame : null;
if (this.state.frameToHighlight !== frameToHighlight) {
flushSync2(() => {
this.setState({ frameToHighlight });
});
}
pointerDownState.drag.hasOccurred = true;
if (selectedElements.length > 0 && !pointerDownState.withCmdOrCtrl && !this.state.editingTextElement && this.state.activeEmbeddable?.state !== "active") {
const dragOffset = {
x: pointerCoords.x - pointerDownState.origin.x,
y: pointerCoords.y - pointerDownState.origin.y
};
const originalElements = [
...pointerDownState.originalElements.values()
];
const lockDirection = event.shiftKey;
if (lockDirection) {
const distanceX = Math.abs(dragOffset.x);
const distanceY = Math.abs(dragOffset.y);
const lockX = lockDirection && distanceX < distanceY;
const lockY = lockDirection && distanceX > distanceY;
if (lockX) {
dragOffset.x = 0;
}
if (lockY) {
dragOffset.y = 0;
}
}
if (this.state.croppingElementId) {
const croppingElement = this.scene.getNonDeletedElementsMap().get(this.state.croppingElementId);
if (croppingElement && isImageElement(croppingElement) && croppingElement.crop !== null && pointerDownState.hit.element === croppingElement) {
const crop = croppingElement.crop;
const image = isInitializedImageElement(croppingElement) && this.imageCache.get(croppingElement.fileId)?.image;
if (image && !(image instanceof Promise)) {
const instantDragOffset = vectorScale(
vector(
pointerCoords.x - lastPointerCoords.x,
pointerCoords.y - lastPointerCoords.y
),
Math.max(this.state.zoom.value, 2)
);
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
croppingElement,
elementsMap
);
const topLeft = vectorFromPoint(
pointRotateRads(
pointFrom(x1, y1),
pointFrom(cx, cy),
croppingElement.angle
)
);
const topRight = vectorFromPoint(
pointRotateRads(
pointFrom(x2, y1),
pointFrom(cx, cy),
croppingElement.angle
)
);
const bottomLeft = vectorFromPoint(
pointRotateRads(
pointFrom(x1, y2),
pointFrom(cx, cy),
croppingElement.angle
)
);
const topEdge = vectorNormalize(
vectorSubtract(topRight, topLeft)
);
const leftEdge = vectorNormalize(
vectorSubtract(bottomLeft, topLeft)
);
const offsetVector = vector(
vectorDot(instantDragOffset, topEdge),
vectorDot(instantDragOffset, leftEdge)
);
const nextCrop = {
...crop,
x: clamp(
crop.x - offsetVector[0] * Math.sign(croppingElement.scale[0]),
0,
image.naturalWidth - crop.width
),
y: clamp(
crop.y - offsetVector[1] * Math.sign(croppingElement.scale[1]),
0,
image.naturalHeight - crop.height
)
};
mutateElement(croppingElement, {
crop: nextCrop
});
return;
}
}
}
this.maybeCacheVisibleGaps(event, selectedElements);
this.maybeCacheReferenceSnapPoints(event, selectedElements);
const { snapOffset, snapLines } = snapDraggedElements(
originalElements,
dragOffset,
this,
event,
this.scene.getNonDeletedElementsMap()
);
this.setState({ snapLines });
if (!this.state.editingFrame) {
dragSelectedElements(
pointerDownState,
selectedElements,
dragOffset,
this.scene,
snapOffset,
event[KEYS.CTRL_OR_CMD] ? null : this.getEffectiveGridSize()
);
}
this.setState({
selectedElementsAreBeingDragged: true,
// element is being dragged and selectionElement that was created on pointer down
// should be removed
selectionElement: null
});
if (selectedElements.length !== 1 || !isElbowArrow(selectedElements[0])) {
this.setState({
suggestedBindings: getSuggestedBindingsForArrows(
selectedElements,
this.scene.getNonDeletedElementsMap(),
this.state.zoom
)
});
}
if (event.altKey && !pointerDownState.hit.hasBeenDuplicated) {
pointerDownState.hit.hasBeenDuplicated = true;
const nextElements = [];
const elementsToAppend = [];
const groupIdMap = /* @__PURE__ */ new Map();
const oldIdToDuplicatedId = /* @__PURE__ */ new Map();
const hitElement = pointerDownState.hit.element;
const selectedElementIds = new Set(
this.scene.getSelectedElements({
selectedElementIds: this.state.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true
}).map((element) => element.id)
);
const elements = this.scene.getElementsIncludingDeleted();
for (const element of elements) {
const isInSelection = selectedElementIds.has(element.id) || // case: the state.selectedElementIds might not have been
// updated yet by the time this mousemove event is fired
element.id === hitElement?.id && pointerDownState.hit.wasAddedToSelection;
if (Math.abs(element.x) > 1e7 || Math.abs(element.x) > 1e7 || Math.abs(element.width) > 1e7 || Math.abs(element.height) > 1e7) {
console.error(
`Alt+dragging element in scene with invalid dimensions`,
element.x,
element.y,
element.width,
element.height,
isInSelection
);
return;
}
if (isInSelection) {
const duplicatedElement = duplicateElement(
this.state.editingGroupId,
groupIdMap,
element
);
if (Math.abs(duplicatedElement.x) > 1e7 || Math.abs(duplicatedElement.x) > 1e7 || Math.abs(duplicatedElement.width) > 1e7 || Math.abs(duplicatedElement.height) > 1e7) {
console.error(
`Alt+dragging duplicated element with invalid dimensions`,
duplicatedElement.x,
duplicatedElement.y,
duplicatedElement.width,
duplicatedElement.height
);
return;
}
const origElement = pointerDownState.originalElements.get(
element.id
);
if (Math.abs(origElement.x) > 1e7 || Math.abs(origElement.x) > 1e7 || Math.abs(origElement.width) > 1e7 || Math.abs(origElement.height) > 1e7) {
console.error(
`Alt+dragging duplicated element with invalid dimensions`,
origElement.x,
origElement.y,
origElement.width,
origElement.height
);
return;
}
mutateElement(duplicatedElement, {
x: origElement.x,
y: origElement.y
});
pointerDownState.originalElements.set(
duplicatedElement.id,
duplicatedElement
);
nextElements.push(duplicatedElement);
elementsToAppend.push(element);
oldIdToDuplicatedId.set(element.id, duplicatedElement.id);
} else {
nextElements.push(element);
}
}
let nextSceneElements = [
...nextElements,
...elementsToAppend
];
const mappedNewSceneElements = this.props.onDuplicate?.(
nextSceneElements,
elements
);
nextSceneElements = mappedNewSceneElements || nextSceneElements;
syncMovedIndices(nextSceneElements, arrayToMap(elementsToAppend));
bindTextToShapeAfterDuplication(
nextElements,
elementsToAppend,
oldIdToDuplicatedId
);
fixBindingsAfterDuplication(
nextSceneElements,
elementsToAppend,
oldIdToDuplicatedId,
"duplicatesServeAsOld"
);
bindElementsToFramesAfterDuplication(
nextSceneElements,
elementsToAppend,
oldIdToDuplicatedId
);
this.scene.replaceAllElements(nextSceneElements);
this.maybeCacheVisibleGaps(event, selectedElements, true);
this.maybeCacheReferenceSnapPoints(event, selectedElements, true);
}
return;
}
}
if (this.state.selectionElement) {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
this.maybeDragNewGenericElement(pointerDownState, event);
} else {
const newElement2 = this.state.newElement;
if (!newElement2) {
return;
}
if (newElement2.type === "freedraw") {
const points = newElement2.points;
const dx = pointerCoords.x - newElement2.x;
const dy = pointerCoords.y - newElement2.y;
const lastPoint = points.length > 0 && points[points.length - 1];
const discardPoint = lastPoint && lastPoint[0] === dx && lastPoint[1] === dy;
if (!discardPoint) {
const pressures = newElement2.simulatePressure ? newElement2.pressures : [...newElement2.pressures, event.pressure];
mutateElement(
newElement2,
{
points: [...points, pointFrom(dx, dy)],
pressures
},
false
);
this.setState({
newElement: newElement2
});
}
} else if (isLinearElement(newElement2)) {
pointerDownState.drag.hasOccurred = true;
const points = newElement2.points;
let dx = gridX - newElement2.x;
let dy = gridY - newElement2.y;
if (shouldRotateWithDiscreteAngle(event) && points.length === 2) {
({ width: dx, height: dy } = getLockedLinearCursorAlignSize(
newElement2.x,
newElement2.y,
pointerCoords.x,
pointerCoords.y
));
}
if (points.length === 1) {
mutateElement(
newElement2,
{
points: [...points, pointFrom(dx, dy)]
},
false
);
} else if (points.length === 2 || points.length > 1 && isElbowArrow(newElement2)) {
mutateElement(
newElement2,
{
points: [...points.slice(0, -1), pointFrom(dx, dy)]
},
false,
{ isDragging: true }
);
}
this.setState({
newElement: newElement2
});
if (isBindingElement(newElement2, false)) {
this.maybeSuggestBindingsForLinearElementAtCoords(
newElement2,
[pointerCoords],
this.state.startBoundElement
);
}
} else {
pointerDownState.lastCoords.x = pointerCoords.x;
pointerDownState.lastCoords.y = pointerCoords.y;
this.maybeDragNewGenericElement(pointerDownState, event, false);
}
}
if (this.state.activeTool.type === "selection") {
pointerDownState.boxSelection.hasOccurred = true;
const elements = this.scene.getNonDeletedElements();
if (this.state.editingLinearElement) {
LinearElementEditor.handleBoxSelection(
event,
this.state,
this.setState.bind(this),
this.scene.getNonDeletedElementsMap()
);
} else {
let shouldReuseSelection = true;
if (!event.shiftKey && isSomeElementSelected(elements, this.state)) {
if (pointerDownState.withCmdOrCtrl && pointerDownState.hit.element) {
this.setState(
(prevState) => selectGroupsForSelectedElements(
{
...prevState,
selectedElementIds: {
[pointerDownState.hit.element.id]: true
}
},
this.scene.getNonDeletedElements(),
prevState,
this
)
);
} else {
shouldReuseSelection = false;
}
}
const elementsWithinSelection = this.state.selectionElement ? getElementsWithinSelection(
elements,
this.state.selectionElement,
this.scene.getNonDeletedElementsMap(),
false
) : [];
this.setState((prevState) => {
const nextSelectedElementIds = {
...shouldReuseSelection && prevState.selectedElementIds,
...elementsWithinSelection.reduce(
(acc, element) => {
acc[element.id] = true;
return acc;
},
{}
)
};
if (pointerDownState.hit.element) {
if (!elementsWithinSelection.length) {
nextSelectedElementIds[pointerDownState.hit.element.id] = true;
} else {
delete nextSelectedElementIds[pointerDownState.hit.element.id];
}
}
prevState = !shouldReuseSelection ? { ...prevState, selectedGroupIds: {}, editingGroupId: null } : prevState;
return {
...selectGroupsForSelectedElements(
{
editingGroupId: prevState.editingGroupId,
selectedElementIds: nextSelectedElementIds
},
this.scene.getNonDeletedElements(),
prevState,
this
),
// select linear element only when we haven't box-selected anything else
selectedLinearElement: elementsWithinSelection.length === 1 && isLinearElement(elementsWithinSelection[0]) ? new LinearElementEditor(elementsWithinSelection[0]) : null,
showHyperlinkPopup: elementsWithinSelection.length === 1 && (elementsWithinSelection[0].link || isEmbeddableElement(elementsWithinSelection[0])) ? "info" : false
};
});
}
}
});
}
// Returns whether the pointer move happened over either scrollbar
handlePointerMoveOverScrollbars(event, pointerDownState) {
if (pointerDownState.scrollbars.isOverHorizontal) {
const x = event.clientX;
const dx = x - pointerDownState.lastCoords.x;
this.translateCanvas({
scrollX: this.state.scrollX - dx / this.state.zoom.value
});
pointerDownState.lastCoords.x = x;
return true;
}
if (pointerDownState.scrollbars.isOverVertical) {
const y = event.clientY;
const dy = y - pointerDownState.lastCoords.y;
this.translateCanvas({
scrollY: this.state.scrollY - dy / this.state.zoom.value
});
pointerDownState.lastCoords.y = y;
return true;
}
return false;
}
onPointerUpFromPointerDownHandler(pointerDownState) {
return withBatchedUpdates((childEvent) => {
this.removePointer(childEvent);
if (pointerDownState.eventListeners.onMove) {
pointerDownState.eventListeners.onMove.flush();
}
const {
newElement: newElement2,
resizingElement,
croppingElementId,
multiElement,
activeTool,
isResizing,
isRotating,
isCropping
} = this.state;
this.setState((prevState) => ({
isResizing: false,
isRotating: false,
isCropping: false,
resizingElement: null,
selectionElement: null,
frameToHighlight: null,
elementsToHighlight: null,
cursorButton: "up",
snapLines: updateStable(prevState.snapLines, []),
originSnapOffset: null
}));
this.lastPointerMoveCoords = null;
SnapCache.setReferenceSnapPoints(null);
SnapCache.setVisibleGaps(null);
this.savePointer(childEvent.clientX, childEvent.clientY, "up");
this.setState({
selectedElementsAreBeingDragged: false
});
const elementsMap = this.scene.getNonDeletedElementsMap();
if (pointerDownState.drag.hasOccurred && pointerDownState.hit?.element?.id) {
const element = elementsMap.get(pointerDownState.hit.element.id);
if (isBindableElement(element)) {
element.boundElements?.filter((e) => e.type === "arrow").map((e) => elementsMap.get(e.id)).filter((e) => isElbowArrow(e)).forEach((e) => {
!!e && mutateElement(e, {}, true);
});
}
}
if (this.state.editingLinearElement) {
if (!pointerDownState.boxSelection.hasOccurred && pointerDownState.hit?.element?.id !== this.state.editingLinearElement.elementId) {
this.actionManager.executeAction(actionFinalize);
} else {
const editingLinearElement = LinearElementEditor.handlePointerUp(
childEvent,
this.state.editingLinearElement,
this.state,
this.scene
);
if (editingLinearElement !== this.state.editingLinearElement) {
this.setState({
editingLinearElement,
suggestedBindings: []
});
}
}
} else if (this.state.selectedLinearElement) {
if (this.state.selectedLinearElement.elbowed) {
const element = LinearElementEditor.getElement(
this.state.selectedLinearElement.elementId,
this.scene.getNonDeletedElementsMap()
);
if (element) {
mutateElement(element, {}, true);
}
}
if (pointerDownState.hit?.element?.id !== this.state.selectedLinearElement.elementId) {
const selectedELements = this.scene.getSelectedElements(this.state);
if (selectedELements.length > 1) {
this.setState({ selectedLinearElement: null });
}
} else {
const linearElementEditor = LinearElementEditor.handlePointerUp(
childEvent,
this.state.selectedLinearElement,
this.state,
this.scene
);
const { startBindingElement, endBindingElement } = linearElementEditor;
const element = this.scene.getElement(linearElementEditor.elementId);
if (isBindingElement(element)) {
bindOrUnbindLinearElement(
element,
startBindingElement,
endBindingElement,
elementsMap,
this.scene
);
}
if (linearElementEditor !== this.state.selectedLinearElement) {
this.setState({
selectedLinearElement: {
...linearElementEditor,
selectedPointsIndices: null
},
suggestedBindings: []
});
}
}
}
this.missingPointerEventCleanupEmitter.clear();
window.removeEventListener(
"pointermove" /* POINTER_MOVE */,
pointerDownState.eventListeners.onMove
);
window.removeEventListener(
"pointerup" /* POINTER_UP */,
pointerDownState.eventListeners.onUp
);
window.removeEventListener(
"keydown" /* KEYDOWN */,
pointerDownState.eventListeners.onKeyDown
);
window.removeEventListener(
"keyup" /* KEYUP */,
pointerDownState.eventListeners.onKeyUp
);
if (this.state.pendingImageElementId) {
this.setState({ pendingImageElementId: null });
}
this.props?.onPointerUp?.(activeTool, pointerDownState);
this.onPointerUpEmitter.trigger(
this.state.activeTool,
pointerDownState,
childEvent
);
if (newElement2?.type === "freedraw") {
const pointerCoords = viewportCoordsToSceneCoords(
childEvent,
this.state
);
const points = newElement2.points;
let dx = pointerCoords.x - newElement2.x;
let dy = pointerCoords.y - newElement2.y;
if (dx === points[0][0] && dy === points[0][1]) {
dy += 1e-4;
dx += 1e-4;
}
const pressures = newElement2.simulatePressure ? [] : [...newElement2.pressures, childEvent.pressure];
mutateElement(newElement2, {
points: [...points, pointFrom(dx, dy)],
pressures,
lastCommittedPoint: pointFrom(dx, dy)
});
this.actionManager.executeAction(actionFinalize);
return;
}
if (isImageElement(newElement2)) {
const imageElement = newElement2;
try {
this.initializeImageDimensions(imageElement);
this.setState(
{
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state
)
},
() => {
this.actionManager.executeAction(actionFinalize);
}
);
} catch (error) {
console.error(error);
this.scene.replaceAllElements(
this.scene.getElementsIncludingDeleted().filter((el) => el.id !== imageElement.id)
);
this.actionManager.executeAction(actionFinalize);
}
return;
}
if (isLinearElement(newElement2)) {
if (newElement2.points.length > 1) {
this.store.shouldCaptureIncrement();
}
const pointerCoords = viewportCoordsToSceneCoords(
childEvent,
this.state
);
if (!pointerDownState.drag.hasOccurred && newElement2 && !multiElement) {
mutateElement(newElement2, {
points: [
...newElement2.points,
pointFrom(
pointerCoords.x - newElement2.x,
pointerCoords.y - newElement2.y
)
]
});
this.setState({
multiElement: newElement2,
newElement: newElement2
});
} else if (pointerDownState.drag.hasOccurred && !multiElement) {
if (isBindingEnabled(this.state) && isBindingElement(newElement2, false)) {
maybeBindLinearElement(
newElement2,
this.state,
pointerCoords,
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements()
);
}
this.setState({ suggestedBindings: [], startBoundElement: null });
if (!activeTool.locked) {
resetCursor(this.interactiveCanvas);
this.setState((prevState) => ({
newElement: null,
activeTool: updateActiveTool(this.state, {
type: "selection"
}),
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[newElement2.id]: true
},
prevState
),
selectedLinearElement: new LinearElementEditor(newElement2)
}));
} else {
this.setState((prevState) => ({
newElement: null
}));
}
this.scene.triggerUpdate();
}
return;
}
if (isTextElement(newElement2)) {
const minWidth = getMinTextElementWidth(
getFontString({
fontSize: newElement2.fontSize,
fontFamily: newElement2.fontFamily
}),
newElement2.lineHeight
);
if (newElement2.width < minWidth) {
mutateElement(newElement2, {
autoResize: true
});
}
this.resetCursor();
this.handleTextWysiwyg(newElement2, {
isExistingElement: true
});
}
if (activeTool.type !== "selection" && newElement2 && isInvisiblySmallElement(newElement2)) {
this.updateScene({
elements: this.scene.getElementsIncludingDeleted().filter((el) => el.id !== newElement2.id),
appState: {
newElement: null
},
captureUpdate: CaptureUpdateAction.NEVER
});
return;
}
if (isFrameLikeElement(newElement2)) {
const elementsInsideFrame = getElementsInNewFrame(
this.scene.getElementsIncludingDeleted(),
newElement2,
this.scene.getNonDeletedElementsMap()
);
this.scene.replaceAllElements(
addElementsToFrame(
this.scene.getElementsMapIncludingDeleted(),
elementsInsideFrame,
newElement2,
this.state
)
);
}
if (newElement2) {
mutateElement(newElement2, getNormalizedDimensions(newElement2));
this.scene.triggerUpdate();
}
if (pointerDownState.drag.hasOccurred) {
const sceneCoords = viewportCoordsToSceneCoords(childEvent, this.state);
if (this.state.selectedLinearElement && this.state.selectedLinearElement.isDragging) {
const linearElement = this.scene.getElement(
this.state.selectedLinearElement.elementId
);
if (linearElement?.frameId) {
const frame = getContainingFrame(linearElement, elementsMap);
if (frame && linearElement) {
if (!elementOverlapsWithFrame(
linearElement,
frame,
this.scene.getNonDeletedElementsMap()
)) {
mutateElement(linearElement, {
groupIds: []
});
removeElementsFromFrame(
[linearElement],
this.scene.getNonDeletedElementsMap()
);
this.scene.triggerUpdate();
}
}
}
} else {
const topLayerFrame = this.getTopLayerFrameAtSceneCoords(sceneCoords);
const selectedElements = this.scene.getSelectedElements(this.state);
let nextElements = this.scene.getElementsMapIncludingDeleted();
const updateGroupIdsAfterEditingGroup = (elements) => {
if (elements.length > 0) {
for (const element of elements) {
const index = element.groupIds.indexOf(
this.state.editingGroupId
);
mutateElement(
element,
{
groupIds: element.groupIds.slice(0, index)
},
false
);
}
nextElements.forEach((element) => {
if (element.groupIds.length && getElementsInGroup(
nextElements,
element.groupIds[element.groupIds.length - 1]
).length < 2) {
mutateElement(
element,
{
groupIds: []
},
false
);
}
});
this.setState({
editingGroupId: null
});
}
};
if (topLayerFrame && !this.state.selectedElementIds[topLayerFrame.id]) {
const elementsToAdd = selectedElements.filter(
(element) => element.frameId !== topLayerFrame.id && isElementInFrame(element, nextElements, this.state)
);
if (this.state.editingGroupId) {
updateGroupIdsAfterEditingGroup(elementsToAdd);
}
nextElements = addElementsToFrame(
nextElements,
elementsToAdd,
topLayerFrame,
this.state
);
} else if (!topLayerFrame) {
if (this.state.editingGroupId) {
const elementsToRemove = selectedElements.filter(
(element) => element.frameId && !isElementInFrame(element, nextElements, this.state)
);
updateGroupIdsAfterEditingGroup(elementsToRemove);
}
}
nextElements = updateFrameMembershipOfSelectedElements(
nextElements,
this.state,
this
);
this.scene.replaceAllElements(nextElements);
}
}
if (resizingElement) {
this.store.shouldCaptureIncrement();
}
if (resizingElement && isInvisiblySmallElement(resizingElement)) {
this.updateScene({
elements: this.scene.getElementsIncludingDeleted().filter((el) => el.id !== resizingElement.id),
captureUpdate: CaptureUpdateAction.NEVER
});
}
if (pointerDownState.resize.isResizing) {
let nextElements = updateFrameMembershipOfSelectedElements(
this.scene.getElementsIncludingDeleted(),
this.state,
this
);
const selectedFrames = this.scene.getSelectedElements(this.state).filter(
(element) => isFrameLikeElement(element)
);
for (const frame of selectedFrames) {
nextElements = replaceAllElementsInFrame(
nextElements,
getElementsInResizingFrame(
this.scene.getElementsIncludingDeleted(),
frame,
this.state,
elementsMap
),
frame,
this
);
}
this.scene.replaceAllElements(nextElements);
}
const hitElement = pointerDownState.hit.element;
if (this.state.selectedLinearElement?.elementId !== hitElement?.id && isLinearElement(hitElement)) {
const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1) {
this.setState({
selectedLinearElement: new LinearElementEditor(hitElement)
});
}
}
if (
// not in the cropping mode at all
!croppingElementId || // in the cropping mode
croppingElementId && // not cropping and no hit element
(!hitElement && !isCropping || // hitting something else
hitElement && hitElement.id !== croppingElementId)
) {
this.finishImageCropping();
}
const pointerStart = this.lastPointerDownEvent;
const pointerEnd = this.lastPointerUpEvent || this.lastPointerMoveEvent;
if (isEraserActive(this.state) && pointerStart && pointerEnd) {
this.eraserTrail.endPath();
const draggedDistance = pointDistance(
pointFrom(pointerStart.clientX, pointerStart.clientY),
pointFrom(pointerEnd.clientX, pointerEnd.clientY)
);
if (draggedDistance === 0) {
const scenePointer = viewportCoordsToSceneCoords(
{
clientX: pointerEnd.clientX,
clientY: pointerEnd.clientY
},
this.state
);
const hitElements = this.getElementsAtPosition(
scenePointer.x,
scenePointer.y
);
hitElements.forEach(
(hitElement2) => this.elementsPendingErasure.add(hitElement2.id)
);
}
this.eraseElements();
return;
} else if (this.elementsPendingErasure.size) {
this.restoreReadyToEraseElements();
}
if (hitElement && !pointerDownState.drag.hasOccurred && !pointerDownState.hit.wasAddedToSelection && // if we're editing a line, pointerup shouldn't switch selection if
// box selected
(!this.state.editingLinearElement || !pointerDownState.boxSelection.hasOccurred)) {
if (childEvent.shiftKey && !this.state.editingLinearElement) {
if (this.state.selectedElementIds[hitElement.id]) {
if (isSelectedViaGroup(this.state, hitElement)) {
this.setState((_prevState) => {
const nextSelectedElementIds = {
..._prevState.selectedElementIds
};
for (const groupedElement of hitElement.groupIds.flatMap(
(groupId) => getElementsInGroup(
this.scene.getNonDeletedElements(),
groupId
)
)) {
delete nextSelectedElementIds[groupedElement.id];
}
return {
selectedGroupIds: {
..._prevState.selectedElementIds,
...hitElement.groupIds.map((gId) => ({ [gId]: false })).reduce((prev, acc) => ({ ...prev, ...acc }), {})
},
selectedElementIds: makeNextSelectedElementIds(
nextSelectedElementIds,
_prevState
)
};
});
} else if (!this.state.selectedLinearElement?.isDragging) {
this.setState((prevState) => {
const newSelectedElementIds = {
...prevState.selectedElementIds
};
delete newSelectedElementIds[hitElement.id];
const newSelectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
{ selectedElementIds: newSelectedElementIds }
);
return {
...selectGroupsForSelectedElements(
{
editingGroupId: prevState.editingGroupId,
selectedElementIds: newSelectedElementIds
},
this.scene.getNonDeletedElements(),
prevState,
this
),
// set selectedLinearElement only if thats the only element selected
selectedLinearElement: newSelectedElements.length === 1 && isLinearElement(newSelectedElements[0]) ? new LinearElementEditor(newSelectedElements[0]) : prevState.selectedLinearElement
};
});
}
} else if (hitElement.frameId && this.state.selectedElementIds[hitElement.frameId]) {
this.setState((prevState) => {
const nextSelectedElementIds = {
...prevState.selectedElementIds,
[hitElement.id]: true
};
delete nextSelectedElementIds[hitElement.frameId];
(this.scene.getElement(hitElement.frameId)?.groupIds ?? []).flatMap(
(gid) => getElementsInGroup(this.scene.getNonDeletedElements(), gid)
).forEach((element) => {
delete nextSelectedElementIds[element.id];
});
return {
...selectGroupsForSelectedElements(
{
editingGroupId: prevState.editingGroupId,
selectedElementIds: nextSelectedElementIds
},
this.scene.getNonDeletedElements(),
prevState,
this
),
showHyperlinkPopup: hitElement.link || isEmbeddableElement(hitElement) ? "info" : false
};
});
} else {
this.setState((_prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
..._prevState.selectedElementIds,
[hitElement.id]: true
},
_prevState
)
}));
}
} else {
this.setState((prevState) => ({
...selectGroupsForSelectedElements(
{
editingGroupId: prevState.editingGroupId,
selectedElementIds: { [hitElement.id]: true }
},
this.scene.getNonDeletedElements(),
prevState,
this
),
selectedLinearElement: isLinearElement(hitElement) && // Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
prevState.selectedLinearElement?.elementId !== hitElement.id ? new LinearElementEditor(hitElement) : prevState.selectedLinearElement
}));
}
}
if (
// not elbow midpoint dragged
!(hitElement && isElbowArrow(hitElement)) && // not dragged
!pointerDownState.drag.hasOccurred && // not resized
!this.state.isResizing && // only hitting the bounding box of the previous hit element
(hitElement && hitElementBoundingBoxOnly(
{
x: pointerDownState.origin.x,
y: pointerDownState.origin.y,
element: hitElement,
shape: getElementShape(
hitElement,
this.scene.getNonDeletedElementsMap()
),
threshold: this.getElementHitThreshold(),
frameNameBound: isFrameLikeElement(hitElement) ? this.frameNameBoundsCache.get(hitElement) : null
},
elementsMap
) || !hitElement && pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements)
) {
if (this.state.editingLinearElement) {
this.setState({ editingLinearElement: null });
} else {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedGroupIds: {},
editingGroupId: null,
activeEmbeddable: null
});
}
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
return;
}
if (!activeTool.locked && activeTool.type !== "freedraw" && newElement2) {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[newElement2.id]: true
},
prevState
),
showHyperlinkPopup: isEmbeddableElement(newElement2) && !newElement2.link ? "editor" : prevState.showHyperlinkPopup
}));
}
if (activeTool.type !== "selection" || isSomeElementSelected(this.scene.getNonDeletedElements(), this.state) || !isShallowEqual(
this.state.previousSelectedElementIds,
this.state.selectedElementIds
)) {
this.store.shouldCaptureIncrement();
}
if (pointerDownState.drag.hasOccurred || isResizing || isRotating || isCropping) {
const linearElements = this.scene.getSelectedElements(this.state).filter(isLinearElement);
bindOrUnbindLinearElements(
linearElements,
this.scene.getNonDeletedElementsMap(),
this.scene.getNonDeletedElements(),
this.scene,
isBindingEnabled(this.state),
this.state.selectedLinearElement?.selectedPointsIndices ?? [],
this.state.zoom
);
}
if (activeTool.type === "laser") {
this.laserTrails.endPath();
return;
}
if (!activeTool.locked && activeTool.type !== "freedraw") {
resetCursor(this.interactiveCanvas);
this.setState({
newElement: null,
suggestedBindings: [],
activeTool: updateActiveTool(this.state, { type: "selection" })
});
} else {
this.setState({
newElement: null,
suggestedBindings: []
});
}
if (hitElement && this.lastPointerUpEvent && this.lastPointerDownEvent && this.lastPointerUpEvent.timeStamp - this.lastPointerDownEvent.timeStamp < 300 && gesture.pointers.size <= 1 && isIframeLikeElement(hitElement) && this.isIframeLikeElementCenter(
hitElement,
this.lastPointerUpEvent,
pointerDownState.origin.x,
pointerDownState.origin.y
)) {
this.handleEmbeddableCenterClick(hitElement);
}
});
}
clearSelection(hitElement) {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),
activeEmbeddable: null,
selectedGroupIds: {},
// Continue editing the same group if the user selected a different
// element from it
editingGroupId: prevState.editingGroupId && hitElement != null && isElementInGroup(hitElement, prevState.editingGroupId) ? prevState.editingGroupId : null
}));
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
activeEmbeddable: null,
previousSelectedElementIds: this.state.selectedElementIds
});
}
getTextWysiwygSnappedToCenterPosition(x, y, appState, container) {
if (container) {
let elementCenterX = container.x + container.width / 2;
let elementCenterY = container.y + container.height / 2;
const elementCenter = getContainerCenter(
container,
appState,
this.scene.getNonDeletedElementsMap()
);
if (elementCenter) {
elementCenterX = elementCenter.x;
elementCenterY = elementCenter.y;
}
const distanceToCenter = Math.hypot(
x - elementCenterX,
y - elementCenterY
);
const isSnappedToCenter = distanceToCenter < TEXT_TO_CENTER_SNAP_THRESHOLD;
if (isSnappedToCenter) {
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: elementCenterX, sceneY: elementCenterY },
appState
);
return { viewportX, viewportY, elementCenterX, elementCenterY };
}
}
}
getCanvasOffsets() {
if (this.excalidrawContainerRef?.current) {
const excalidrawContainer = this.excalidrawContainerRef.current;
const { left, top } = excalidrawContainer.getBoundingClientRect();
return {
offsetLeft: left,
offsetTop: top
};
}
return {
offsetLeft: 0,
offsetTop: 0
};
}
async updateLanguage() {
const currentLang2 = languages.find((lang) => lang.code === this.props.langCode) || defaultLang;
await setLanguage(currentLang2);
this.setAppState({});
}
};
var createTestHook = () => {
if (define_import_meta_env_default.MODE === ENV.TEST || define_import_meta_env_default.DEV) {
window.h = window.h || {};
Object.defineProperties(window.h, {
elements: {
configurable: true,
get() {
return this.app?.scene.getElementsIncludingDeleted();
},
set(elements) {
return this.app?.scene.replaceAllElements(
syncInvalidIndices(elements)
);
}
},
scene: {
configurable: true,
get() {
return this.app?.scene;
}
}
});
}
};
createTestHook();
var App_default = App;
// polyfill.ts
var polyfill = () => {
if (!Array.prototype.at) {
Object.defineProperty(Array.prototype, "at", {
value: function(n) {
n = Math.trunc(n) || 0;
if (n < 0) {
n += this.length;
}
if (n < 0 || n >= this.length) {
return void 0;
}
return this[n];
},
writable: true,
enumerable: false,
configurable: true
});
}
if (!Element.prototype.replaceChildren) {
Element.prototype.replaceChildren = function(...nodes) {
this.innerHTML = "";
this.append(...nodes);
};
}
};
var polyfill_default = polyfill;
// components/footer/FooterCenter.tsx
import clsx56 from "clsx";
import { jsx as jsx148 } from "react/jsx-runtime";
var FooterCenter = ({ children }) => {
const { FooterCenterTunnel } = useTunnels();
const appState = useUIAppState();
return /* @__PURE__ */ jsx148(FooterCenterTunnel.In, { children: /* @__PURE__ */ jsx148(
"div",
{
className: clsx56("footer-center zen-mode-transition", {
"layer-ui__wrapper__footer-left--transition-bottom": appState.zenModeEnabled
}),
children
}
) });
};
var FooterCenter_default = FooterCenter;
FooterCenter.displayName = "FooterCenter";
// components/ExcalidrawLogo.tsx
import { jsx as jsx149, jsxs as jsxs79 } from "react/jsx-runtime";
var LogoIcon = () => /* @__PURE__ */ jsx149(
"svg",
{
viewBox: "0 0 40 40",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
className: "ExcalidrawLogo-icon",
children: /* @__PURE__ */ jsx149(
"path",
{
d: "M39.9 32.889a.326.326 0 0 0-.279-.056c-2.094-3.083-4.774-6-7.343-8.833l-.419-.472a.212.212 0 0 0-.056-.139.586.586 0 0 0-.167-.111l-.084-.083-.056-.056c-.084-.167-.28-.278-.475-.167-.782.39-1.507.973-2.206 1.528-.92.722-1.842 1.445-2.708 2.25a8.405 8.405 0 0 0-.977 1.028c-.14.194-.028.361.14.444-.615.611-1.23 1.223-1.843 1.861a.315.315 0 0 0-.084.223c0 .083.056.166.111.194l1.09.833v.028c1.535 1.528 4.244 3.611 7.12 5.861.418.334.865.667 1.284 1 .195.223.39.473.558.695.084.11.28.139.391.055.056.056.14.111.196.167a.398.398 0 0 0 .167.056.255.255 0 0 0 .224-.111.394.394 0 0 0 .055-.167c.029 0 .028.028.056.028a.318.318 0 0 0 .224-.084l5.082-5.528a.309.309 0 0 0 0-.444Zm-14.63-1.917a.485.485 0 0 0 .111.14c.586.5 1.2 1 1.843 1.555l-2.569-1.945-.251-.166c-.056-.028-.112-.084-.168-.111l-.195-.167.056-.056.055-.055.112-.111c.866-.861 2.346-2.306 3.1-3.028-.81.805-2.43 3.167-2.095 3.944Zm8.767 6.89-2.122-1.612a44.713 44.713 0 0 0-2.625-2.5c1.145.861 2.122 1.611 2.262 1.75 1.117.972 1.06.806 1.815 1.445l.921.666a1.06 1.06 0 0 1-.251.25Zm.558.416-.056-.028c.084-.055.168-.111.252-.194l-.196.222ZM1.089 5.75c.055.361.14.722.195 1.056.335 1.833.67 3.5 1.284 4.75l.252.944c.084.361.223.806.363.917 1.424 1.25 3.602 3.11 5.947 4.889a.295.295 0 0 0 .363 0s0 .027.028.027a.254.254 0 0 0 .196.084.318.318 0 0 0 .223-.084c2.988-3.305 5.221-6.027 6.813-8.305.112-.111.14-.278.14-.417.111-.111.195-.25.307-.333.111-.111.111-.306 0-.39l-.028-.027c0-.055-.028-.139-.084-.167-.698-.666-1.2-1.138-1.731-1.638-.922-.862-1.871-1.75-3.881-3.75l-.028-.028c-.028-.028-.056-.056-.112-.056-.558-.194-1.703-.389-3.127-.639C6.087 2.223 3.21 1.723.614.944c0 0-.168 0-.196.028l-.083.084c-.028.027-.056.055-.224.11h.056-.056c.028.167.028.278.084.473 0 .055.112.5.112.555l.782 3.556Zm15.496 3.278-.335-.334c.084.112.196.195.335.334Zm-3.546 4.666-.056.056c0-.028.028-.056.056-.056Zm-2.038-10c.168.167.866.834 1.033.973-.726-.334-2.54-1.167-3.379-1.445.838.167 1.983.334 2.346.472ZM1.424 2.306c.419.722.754 3.222 1.089 5.666-.196-.778-.335-1.555-.503-2.278-.251-1.277-.503-2.416-.838-3.416.056 0 .14 0 .252.028Zm-.168-.584c-.112 0-.223-.028-.307-.028 0-.027 0-.055-.028-.055.14 0 .223.028.335.083Zm-1.089.222c0-.027 0-.027 0 0ZM39.453 1.333c.028-.11-.558-.61-.363-.639.42-.027.42-.666 0-.666-.558.028-1.144.166-1.675.25-.977.194-1.982.389-2.96.61-2.205.473-4.383.973-6.561 1.557-.67.194-1.424.333-2.066.666-.224.111-.196.333-.084.472-.056.028-.084.028-.14.056-.195.028-.363.056-.558.083-.168.028-.252.167-.224.334 0 .027.028.083.028.11-1.173 1.556-2.485 3.195-3.909 4.945-1.396 1.611-2.876 3.306-4.356 5.056-4.719 5.5-10.052 11.75-15.943 17.25a.268.268 0 0 0 0 .389c.028.027.056.055.084.055-.084.084-.168.14-.252.222-.056.056-.084.111-.084.167a.605.605 0 0 0-.111.139c-.112.111-.112.305.028.389.111.11.307.11.39-.028.029-.028.029-.056.056-.056a.44.44 0 0 1 .615 0c.335.362.67.723.977 1.028l-.698-.583c-.112-.111-.307-.083-.39.028-.113.11-.085.305.027.389l7.427 6.194c.056.056.112.056.196.056s.14-.028.195-.084l.168-.166c.028.027.083.027.111.027.084 0 .14-.027.196-.083 10.052-10.055 18.15-17.639 27.42-24.417.083-.055.111-.166.111-.25.112 0 .196-.083.251-.194 1.704-5.194 2.039-9.806 2.15-12.083v-.028c0-.028.028-.056.028-.083.028-.056.028-.084.028-.084a1.626 1.626 0 0 0-.111-1.028ZM21.472 9.5c.446-.5.893-1.028 1.34-1.5-2.876 3.778-7.65 9.583-14.408 16.5 4.607-5.083 9.242-10.333 13.068-15ZM5.193 35.778h.084-.084Zm3.462 3.194c-.027-.028-.027-.028 0-.028v.028Zm4.16-3.583c.224-.25.448-.472.699-.722 0 0 0 .027.028.027-.252.223-.475.445-.726.695Zm1.146-1.111c.14-.14.279-.334.446-.5l.028-.028c1.648-1.694 3.351-3.389 5.082-5.111l.028-.028c.419-.333.921-.694 1.368-1.028a379.003 379.003 0 0 0-6.952 6.695ZM24.794 6.472c-.921 1.195-1.954 2.778-2.82 4.028-2.736 3.944-11.532 13.583-11.727 13.75a1976.983 1976.983 0 0 1-8.042 7.639l-.167.167c-.14-.167-.14-.417.028-.556C14.49 19.861 22.03 10.167 25.074 5.917c-.084.194-.14.36-.28.555Zm4.83 5.695c-1.116-.64-1.646-1.64-1.34-2.611l.084-.334c.028-.083.084-.194.14-.277.307-.5.754-.917 1.257-1.167.027 0 .055 0 .083-.028-.028-.056-.028-.139-.028-.222.028-.167.14-.278.335-.278.335 0 1.369.306 1.76.639.111.083.223.194.335.305.14.167.363.445.474.667.056.028.112.306.196.445.056.222.111.472.084.694-.028.028 0 .194-.028.194a2.668 2.668 0 0 1-.363 1.028c-.028.028-.028.056-.056.084l-.028.027c-.14.223-.335.417-.53.556-.643.444-1.369.583-2.095.389 0 0-.195-.084-.28-.111Zm8.154-.834a39.098 39.098 0 0 1-.893 3.167c0 .028-.028.083 0 .111-.056 0-.084.028-.14.056-2.206 1.61-4.356 3.305-6.506 5.028 1.843-1.64 3.686-3.306 5.613-4.945.558-.5.949-1.139 1.06-1.861l.28-1.667v-.055c.14-.334.67-.195.586.166Z",
fill: "currentColor"
}
)
}
);
var LogoText = () => /* @__PURE__ */ jsxs79(
"svg",
{
viewBox: "0 0 450 55",
xmlns: "http://www.w3.org/2000/svg",
fill: "none",
className: "ExcalidrawLogo-text",
children: [
/* @__PURE__ */ jsx149(
"path",
{
d: "M429.27 96.74c2.47-1.39 4.78-3.02 6.83-4.95 1.43-1.35 2.73-2.86 3.81-4.51-.66.9-1.4 1.77-2.23 2.59-2.91 2.84-5.72 5.09-8.42 6.87h.01ZM343.6 69.36c.33 3.13.58 6.27.79 9.4.09 1.37.18 2.75.25 4.12-.12-4.46-.27-8.93-.5-13.39-.11-2.08-.24-4.16-.4-6.24-.06 1.79-.11 3.85-.13 6.11h-.01ZM378.47 98.34c.01-.37.07-1.13.01-6.51-.11 1.9-.22 3.81-.31 5.71-.07 1.42-.22 2.91-.16 4.35.39.03.78.07 1.17.1-.92-.85-.76-2.01-.72-3.66l.01.01ZM344.09 86.12c-.09-2.41-.22-4.83-.39-7.24v12.21c.15-.05.32-.09.47-.14.05-1.61-.03-3.23-.09-4.83h.01ZM440.69 66.79c-.22-.34-.45-.67-.69-.99-3.71-4.87-9.91-7.14-15.65-8.55-1.05-.26-2.12-.49-3.18-.71 2.29.59 4.48 1.26 6.64 2.02 7.19 2.54 10.57 5.41 12.88 8.23ZM305.09 72.46l1.2 3.6c.84 2.53 1.67 5.06 2.46 7.61.24.78.5 1.57.73 2.36.22-.04.44-.08.67-.12a776.9 776.9 0 0 1-5.01-13.57c-.02.04-.03.09-.05.13v-.01ZM345.49 90.25v.31c1.48-.42 3.05-.83 4.66-1.2-1.56.25-3.12.52-4.66.89ZM371.02 90.22c0-.57-.04-1.14-.11-1.71-.06-.02-.12-.04-.19-.05-.21-.05-.43-.08-.65-.11.42.16.74.88.95 1.87ZM398.93 54.23c-.13 0-.27-.01-.4-.02l.03.4c.11-.15.23-.27.37-.38ZM401.57 62.28v-.15c-1.22-.24-2.86-.61-3.23-1.25-.09-.15-.18-.51-.27-.98-.09.37-.2.73-.33 1.09 1.24.56 2.52.98 3.83 1.29ZM421.73 88.68c-2.97 1.65-6.28 3.12-9.69 3.68v.18c4.72-.14 11.63-3.85 16.33-8.38-2.04 1.75-4.33 3.24-6.63 4.53l-.01-.01ZM411.28 80.92c-.05-1.2-.09-2.4-.15-3.6-.21 5.66-.46 11.38-.47 14.51.24-.02.48-.04.71-.07.15-3.61.05-7.23-.09-10.83v-.01Z",
transform: "translate(-144.023 -51.76)"
}
),
/* @__PURE__ */ jsx149(
"path",
{
d: "M425.38 67.41c-3.5-1.45-7.19-2.57-14.06-3.62.09 1.97.06 4.88-.03 8.12.03.04.06.09.06.15.19 1.36.28 2.73.37 4.1.25 3.77.39 7.55.41 11.33 0 1.38-.01 2.76-.07 4.13 1.4-.25 2.78-.65 4.12-1.15 4.07-1.5 7.94-3.78 11.28-6.54 2.33-1.92 5.13-4.49 5.88-7.58.63-3.53-2.45-6.68-7.97-8.96l.01.02ZM411.35 92.53v-.06l-.34.03c.11.01.22.03.34.03ZM314.26 64.06c-.23-.59-.47-1.17-.7-1.75.57 1.62 1.11 3.25 1.6 4.9l.15.54 2.35 6.05c.32.82.66 1.64.98 2.46-1.38-4.1-2.83-8.17-4.39-12.2h.01ZM156.82 103.07c-.18.13-.38.23-.58.33 1.32-.03 2.66-.2 3.93-.34.86-.09 1.72-.22 2.58-.33-2.12.1-4.12.17-5.94.34h.01ZM210.14 68.88s.03.04.05.07c.18-.31.39-.64.58-.96-.21.3-.42.6-.64.89h.01ZM201.65 82.8c-.5.77-1.02 1.56-1.49 2.37 1.11-1.55 2.21-3.1 3.2-4.59-.23.23-.49.51-.75.79-.32.47-.65.95-.96 1.43ZM194.03 98.66c-.33-.4-.65-.84-1.05-1.17-.24-.2-.07-.49.17-.56-.23-.26-.42-.5-.63-.75 1.51-2.55 3.93-5.87 6.4-9.28-.17-.08-.29-.28-.2-.49.04-.09.09-.17.13-.26-1.21 1.78-2.42 3.55-3.61 5.33-.87 1.31-1.74 2.64-2.54 4-.29.5-.63 1.04-.87 1.61.81.65 1.63 1.27 2.47 1.88-.09-.11-.18-.21-.27-.32v.01ZM307.79 82.93c-1-3.17-2.05-6.32-3.1-9.48-1.62 4.08-3.69 9.17-6.16 15.19 3.32-1.04 6.77-1.87 10.27-2.5-.32-1.08-.67-2.15-1.01-3.21ZM149.5 80.7c.05-1.71.04-3.43 0-5.14-.1 2.26-.16 4.51-.22 6.77-.02.73-.03 1.46-.04 2.19.14-1.27.2-2.55.24-3.82h.02ZM228.98 98.3c.39 1.25.91 3.03.94 3.91.06-.03.12-.07.17-.1.08-1.29-.55-2.65-1.11-3.81ZM307.72 53.36c.81.5 1.53 1.04 2.07 1.49-.38-.8-.78-1.58-1.21-2.35-.17.03-.34.06-.51.11-.43.12-.86.26-1.29.41.35-.01.53.1.94.34ZM283.69 96.14c3.91-7.25 6.89-13.35 8.88-18.15l1.1-2.66c-1.27 2.64-2.56 5.27-3.83 7.9-1.53 3.15-3.06 6.31-4.58 9.47-.87 1.81-1.76 3.62-2.54 5.47.04.02.07.04.11.07.05.05.1.09.15.14.05-.73.27-1.48.71-2.24ZM289.92 103.23s-.04.01-.05.03c0-.02.04-.03.05-.04.05-.05.11-.1.16-.15l.21-.21c-.55 0-1.5-.27-2.55-.72.4.26.8.51 1.22.74.24.13.48.26.73.37.05.02.1.03.14.05a.27.27 0 0 1 .08-.07h.01ZM269.23 68.49c-.39-.19-.82-.48-1.33-.87-3.06-1.56-6.31-2.78-9.36-2.35-3.5.49-5.7 1.11-7.74 2.44 5.71-2.6 12.82-2.07 18.44.79l-.01-.01ZM177.87 53.69l1.06.03c-.96-.22-2-.25-2.89-.3-4.95-.26-9.99.33-14.86 1.19-2.44.43-4.88.95-7.28 1.59 9.09-1.76 15.69-2.77 23.97-2.51ZM219.85 55.51c-.18.12-.36.27-.56.45-.45.53-.86 1.11-1.26 1.66-1.91 2.61-3.71 5.31-5.57 7.95l-.12.18 8.05-10.11c-.18-.05-.36-.1-.55-.13h.01ZM510.71 54.1c.12-.15.29-.3.53-.45.69-.4 3.72-.63 5.87-.74-.36-.02-.73-.04-1.09-.05-1.84-.03-3.67.09-5.49.35.05.3.12.59.18.88v.01ZM510.76 86.02c1.37-3.07 2.49-6.27 3.57-9.46.55-1.64 1.12-3.3 1.6-4.97-1.59 4.01-3.67 9.14-6.2 15.3.24-.08.5-.14.74-.22.1-.22.19-.44.29-.65ZM566.95 75.76c.11-.02.23.03.31.11-.05-.13-.09-.26-.14-.39-.05.09-.11.18-.17.28ZM511.33 86.41c3.08-.89 6.24-1.62 9.46-2.14-1.51-3.98-2.98-7.96-4.39-11.87-.05.15-.09.31-.14.46-1.02 3.32-2.15 6.61-3.39 9.85-.48 1.25-.98 2.49-1.53 3.7h-.01ZM578.24 74.45c.11-.44.23-.87.35-1.31-.31.7-.64 1.39-.97 2.08.09.21.19.4.28.61.12-.46.23-.92.35-1.38h-.01ZM520.62 53.11c-.09 0-.18-.01-.28-.02.38.34.29 1.08.93 2.53l6.65 17.15c2.2 5.68 4.69 11.36 7.41 16.87l1.06 2.17c-2.95-7.05-5.92-14.08-8.87-21.13-1.58-3.79-3.16-7.59-4.7-11.4-.78-1.92-1.73-3.89-2.25-5.91-.03-.1 0-.19.04-.26h.01ZM578.78 77.87c1.45-5.77 3.07-10.43 3.58-13.36.05-.34.16-.88.31-1.55-.67 1.79-1.37 3.56-2.08 5.33-.12.43-.23.86-.35 1.29-.65 2.43-1.29 4.86-1.9 7.3.14.33.29.65.43 1l.01-.01ZM545.3 94.66c.02-.44.03-.83.05-1.12.02-1.01.05-2.02.11-3.02.03-6.66-.46-14.33-1.46-22.8-.13-.42-.27-1.24-.56-2.89 0-.02 0-.04-.01-.06.62 6.61.95 13.25 1.32 19.87.17 3.08.33 6.16.52 9.23.02.25.03.52.04.78l-.01.01ZM580.77 102.81c.13.2.27.38.37.49.27-.11.53-.22.8-.32-.43.09-.82.05-1.17-.16v-.01ZM530.48 104.07h.33c-.36-.13-.71-.32-1.04-.56.14.24.3.47.45.7.06-.08.14-.13.26-.13v-.01ZM542.63 58.82c.06.23.11.47.15.71.14-.33.36-.62.7-.86-.28.05-.57.11-.85.15ZM583.81 57.87c.15-.7.29-1.41.42-2.11-.14.45-.28.9-.42 1.34-.46 1.44-.89 2.89-1.31 4.34.44-1.19.88-2.37 1.31-3.57ZM523.62 91.48c-4.66 1.17-9.05 2.89-14.02 5.27 4.65-1.84 9.48-3.29 14.28-4.63-.09-.22-.17-.41-.26-.64ZM460.64 78.3c-.04-2.9-.11-5.81-.28-8.71-.1-1.68-.17-3.43-.5-5.09-.07.02-.14.03-.2.05.3 6.54.45 12.17.51 17.12.17-.07.34-.14.51-.2 0-1.06-.01-2.11-.03-3.17h-.01ZM470.63 63.24c-3.38-.26-6.81.32-10.1 1.1.41 2.01.47 4.14.57 6.18.18 3.55.25 7.11.27 10.67 3.31-1.38 6.5-3.12 9.3-5.35 1.96-1.56 3.86-3.41 5.02-5.66.73-1.41 1.19-3.22.26-4.65-1.09-1.7-3.46-2.14-5.32-2.29ZM460.29 63.68c1-.24 2.01-.46 3.04-.65-1.15.16-2.37.38-3.71.69v.13c.07-.02.15-.04.22-.05.11-.13.3-.18.45-.11v-.01ZM457.24 100.96c.43-.03.86-.07 1.29-.11.14-.49.27-.99.38-1.49-.44.7-1 1.23-1.67 1.6ZM482.88 104.98c-.18.23-.36.38-.55.47.14.09.27.19.4.28a70.76 70.76 0 0 0 4.37-4.63c.76-.89 1.52-1.81 2.19-2.77-.3-.27-.61-.53-.92-.79-.07 1.94-4.62 6.32-5.49 7.45v-.01Z",
transform: "translate(-144.023 -51.76)"
}
),
/* @__PURE__ */ jsx149(
"path",
{
d: "M474.36 63.31c-.4-.16-.84-.27-1.29-.37 1.56.42 3.08 1.22 3.76 2.74.62 1.4.32 2.95-.28 4.32.7-1.22.94-2.34.74-3.47-.24-1.33-1.19-2.54-2.93-3.21v-.01ZM477.34 89.18c-1.2-.81-2.4-1.62-3.6-2.42-.14.1-.26.19-.4.29 1.4.67 2.73 1.39 4 2.13ZM465.88 93.85c.37.25.74.5 1.1.75.46.32.92.65 1.38.97-1.57-1.2-2.01-1.61-2.49-1.72h.01ZM574.92 90.06c-2.28-5.21-4.93-11.13-5.67-12.26-.1-.15-1.57-3.01-1.63-3.08 0 0-.01.02-.02.02.4 1.37 1.09 2.69 1.65 3.99 2.14 4.95 4.36 9.86 6.67 14.73.6 1.26 1.21 2.52 1.83 3.78-.75-2.01-1.64-4.45-2.83-7.18ZM448.73 65.29c.1.2.22.38.34.57.22-.02.43-.06.65-.08v-.08c-.14-.05-.25 0-.99-.41ZM460.16 94.81c-.02.31-.06.59-.1.89-.03 1.71-.33 3.43-.79 5.07.15-.02.3-.03.45-.05.01-.04.02-.08.03-.11.09-.34.15-.69.2-1.03.17-1.07.25-2.16.33-3.24.05-.69.08-1.39.12-2.08-.27.1-.27.26-.24.57v-.02Z",
transform: "translate(-144.023 -51.76)"
}
),
/* @__PURE__ */ jsx149(
"path",
{
d: "m328.67 98.12-3.22-6.58c-1.29-2.63-2.53-5.29-3.72-7.97-.25-.85-.52-1.69-.79-2.53-.81-2.57-1.67-5.12-2.55-7.67-1.92-5.53-3.9-11.08-6.32-16.41-.72-1.58-1.46-3.44-2.63-4.79-.03-.17-.16-.29-.34-.36a.282.282 0 0 0-.23-.04c-.06-.01-.12 0-.18.01-.74.06-1.5.38-2.19.61-2.22.77-4.4 1.64-6.63 2.38-.03-.08-.06-.16-.09-.25-.15-.42-.82-.24-.67.19.03.09.07.19.1.28l-.18.06c-.36.11-.28.6 0 .68.18 1.18.63 2.36.98 3.49.03.09.06.17.08.26-.08.23-.17.46-.24.64-.37.98-.79 1.94-1.21 2.9-1.27 2.89-2.62 5.75-3.98 8.6-3.18 6.67-6.44 13.31-9.64 19.97-1.08 2.25-2.2 4.5-3.15 6.81-.13.32.24.5.5.37 1.34 1.33 2.84 2.5 4.4 3.57.65.44 1.31.87 2.01 1.24.4.22.86.48 1.33.5.24.01.35-.19.33-.37.11-.1.21-.21.28-.28.41-.41.81-.84 1.2-1.26.85-.92 1.69-1.87 2.5-2.84 6.31-2.34 12.6-4.31 18.71-5.84 2.14 5.3 3.43 8.43 3.97 9.58.55 1.05 1.15 1.88 1.82 2.52 1.32.56 6.96-.03 9.23-1.96.87-1.28 1.19-2.67.93-4.15-.09-.5-.22-.95-.4-1.33l-.01-.03Zm-20.09-45.61c.43.77.83 1.56 1.21 2.35-.54-.45-1.27-.99-2.07-1.49-.42-.24-.6-.35-.94-.34.43-.15.85-.29 1.29-.41.17-.05.34-.08.51-.11Zm-25.86 45.66c.78-1.85 1.67-3.66 2.54-5.47 1.51-3.16 3.05-6.31 4.58-9.47 1.28-2.63 2.56-5.26 3.83-7.9l-1.1 2.66c-1.99 4.79-4.97 10.9-8.88 18.15-.43.76-.66 1.51-.71 2.24-.05-.05-.1-.09-.15-.14a.259.259 0 0 0-.11-.07Zm6.24 4.71c-.42-.23-.82-.48-1.22-.74 1.05.45 2 .72 2.55.72l-.21.21c-.05.05-.11.1-.16.15-.01.01-.04.03-.05.04 0-.02.03-.02.05-.03a.27.27 0 0 0-.08.07c-.05-.02-.1-.03-.14-.05-.25-.1-.49-.24-.73-.37h-.01Zm15.73-29.43c1.05 3.15 2.1 6.31 3.1 9.48.34 1.06.69 2.13 1.01 3.21-3.5.63-6.95 1.46-10.27 2.5 2.48-6.03 4.54-11.11 6.16-15.19Zm4.79 12.57c-.23-.79-.49-1.58-.73-2.36-.79-2.54-1.63-5.08-2.46-7.61l-1.2-3.6c.02-.04.04-.09.05-.13 1.6 4.45 3.28 9 5.01 13.57l-.67.12v.01Zm5.83-18.27-.15-.54c-.49-1.64-1.03-3.28-1.6-4.9.23.58.47 1.17.7 1.75 1.56 4.03 3.01 8.1 4.39 12.2-.33-.82-.67-1.64-.98-2.46l-2.35-6.05h-.01ZM390.43 79.37c-.13-10.43-.22-17.5-.24-19.97-.24-1.6.21-2.88-.65-3.65-.14-.13-.32-.23-.52-.32h.03c.45 0 .45-.69 0-.7-1.75-.03-3.5-.04-5.25-.14-1.38-.08-2.76-.21-4.15-.31-.07 0-.12.01-.17.04-.21-.07-.47.03-.45.31l.03.45c-.11.14-.19.3-.22.5-.21 1.26-.32 13.67-.36 23.59-.32 5.79-.67 11.57-.97 17.36-.09 1.73-.29 3.54-.21 5.3-.39.02-.38.64.04.69v.12c.05.44.74.45.7 0v-.06c1.1.09 2.2.21 3.3.3 1.14.19 2.44.2 3.29.17 1.73-.05 2.92-.05 3.8-.37.45-.05.9-.11 1.35-.17.44-.06.25-.73-.19-.67h-.01c.24-.32.45-.72.62-1.25.66-1.84.41-6.36.34-11.33l-.13-9.9.02.01Zm-12.26 18.17c.09-1.91.2-3.81.31-5.71.06 5.38 0 6.14-.01 6.51-.05 1.65-.21 2.81.72 3.66-.39-.04-.78-.07-1.17-.1-.06-1.44.09-2.93.16-4.35l-.01-.01ZM588.97 53.85c-2.06-.25-3.17-.51-3.76-.6a.3.3 0 0 1 .04-.08c.22-.39-.39-.75-.6-.35-.56 1.02-.9 2.19-1.26 3.29-.61 1.88-1.17 3.78-1.72 5.68-.63 2.19-1.24 4.39-1.83 6.59-.81 2.03-1.67 4.05-2.61 6.03-1.7-3.64-3.11-6.04-4.03-7.57-2.26-3.74-2.85-5.48-3.57-6.08l.31-.09c.43-.12.25-.8-.19-.67-1.06.3-2.12.6-3.17.95-.93.32-1.85.69-2.76 1.07-.13.05-.19.16-.22.27-.04.02-.08.05-.11.07-.04-.06-.07-.12-.11-.18a.354.354 0 0 0-.48-.12c-.16.09-.22.32-.13.48l.33.54c0 .09.02.18.06.28.51 1.16.78 1.38.72 1.47-2.42 3.44-5.41 7.86-6.2 9.1-1.27 1.97-2.01 3.14-2.45 3.84l-.91-6.56-.43-4.1c-.19-1.85-.37-3.23-.53-4.13-.19-1.1-.3-2.15-.45-3.16-.2-1.36-.29-2.06-.47-2.42h.04c.45.02.45-.68 0-.7-3.43-.16-6.81.94-10.17 1.48-.24-.22-.73-.04-.58.32.24.59.33 1.25.43 1.87.17 1.06.29 2.13.4 3.2.32 3.09.53 6.2.74 9.3.44 6.75.77 13.51 1.17 20.26.11 1.95.13 3.96.46 5.89.05.3.37.31.55.14.74 1.71 2.87 1.27 6.13 1.27 1.34 0 2.39.04 2.99-.11.02.32.48.53.63.18 3.61-8.26 7.41-16.46 12.05-24.2.03-.05.04-.1.05-.15.3.73.64 1.45.94 2.16.97 2.26 1.97 4.52 2.98 6.76 2.26 5.03 4.54 10.07 7.09 14.96.47.9.94 1.79 1.47 2.65.2.32.4.67.66.96-.18.25 0 .68.34.54.91-.38 1.82-.75 2.76-1.07 1.04-.35 2.11-.65 3.17-.95.39-.11.28-.66-.07-.68.62-.4.95-.96.87-1.91-.3-3.34.72-7.47.86-8.52l2.14-11.43c1.75-10.74 3.13-17.51 3.23-20.86.02-.49.08-2.84.13-3.24.17-1.25.48-1-4.96-1.65l.03-.02Zm-46.19 5.67c-.04-.24-.09-.48-.15-.71l.85-.15c-.34.24-.56.53-.7.86Zm1.95 25.12c-.36-6.63-.7-13.26-1.32-19.87 0 .02 0 .04.01.06.29 1.65.44 2.47.56 2.89 1 8.46 1.5 16.14 1.46 22.8-.06.99-.1 2-.11 3.02-.01.29-.03.68-.05 1.12-.01-.26-.03-.53-.04-.78-.19-3.08-.35-6.16-.52-9.23l.01-.01Zm36.4 18.66c-.11-.11-.24-.29-.37-.49.35.21.74.26 1.17.16-.27.11-.53.22-.8.32v.01Zm-.89-33.72c.12-.43.23-.86.35-1.29.71-1.77 1.41-3.55 2.08-5.33-.15.68-.26 1.22-.31 1.55-.5 2.94-2.13 7.59-3.58 13.36-.15-.35-.29-.66-.43-1 .61-2.44 1.25-4.87 1.9-7.3l-.01.01Zm3.56-12.48c.14-.44.28-.89.42-1.34-.13.7-.27 1.41-.42 2.11-.43 1.19-.86 2.38-1.31 3.57.42-1.45.85-2.9 1.31-4.34Zm-5.22 16.05c-.11.44-.23.87-.35 1.31-.12.46-.23.92-.35 1.38-.1-.22-.19-.4-.28-.61.34-.69.66-1.38.97-2.08h.01Zm-11.64 2.62c.06-.1.12-.19.17-.28.05.13.09.26.14.39a.398.398 0 0 0-.31-.11Zm2.3 2.98c-.56-1.3-1.25-2.63-1.65-3.99 0 0 .01-.02.02-.02.06.08 1.52 2.93 1.63 3.08.73 1.13 3.38 7.04 5.67 12.26 1.2 2.73 2.08 5.17 2.83 7.18-.62-1.25-1.23-2.51-1.83-3.78-2.31-4.87-4.53-9.78-6.67-14.73ZM275.92 87.03c-1.06-2.18-1.13-3.45-2.44-2.93-1.52.57-2.94 1.3-4.5 2.1-1.4.72-2.68 1.44-3.92 2.12.01-.25-.24-.5-.51-.34-4.8 2.93-12.41 4.7-17.28 1.31-1.98-1.77-3.32-4.15-3.97-5.78-.29-.95-.49-1.94-.63-2.93-.14-3.34 1.58-6.53 3.9-9.12.8-.79 1.68-1.51 2.66-2.12 3.7-2.3 8.22-3.07 12.51-2.51 2.71.35 5.32 1.24 7.71 2.55.39.22.75-.39.35-.6-.18-.1-.37-.18-.55-.27.56.27 1.03.33 1.51.19l-.48.39c-.15.11-.23.3-.13.48.09.15.33.24.48.13 1.3-.97 2.46-2.09 3.45-3.37.37-.29.64-.6.65-.97v-.02c.08-.33-.03-.7-.21-1.08-.31-.87-.98-2.01-2.19-3.26-2.43-2.52-3.79-3.45-5.68-4.26-1.14-.49-3.12-1.06-4.42-1.23-3.28-.42-10.64-1.21-18.18 4.11-7.74 5.46-11.94 12.3-12.23 20.61-.08 2.06.04 3.98.34 5.71.74 4.18 2.57 8 5.44 11.34 4.26 4.99 9.76 7.52 16.34 7.52 4.85 0 9.69-1.77 14.89-4.62.23-.12.45-.23.68-.35 2.19-1.1 4.37-2.23 6.46-3.5.49-.3 1.03-.61 1.5-.98 1.47-.87 1.11-1.12.49-2.95-.39-1.14-.76-2.7-2.06-5.36l.02-.01Zm-17.38-21.76c3.05-.42 6.31.79 9.36 2.35.51.39.94.68 1.33.87-5.61-2.86-12.72-3.39-18.44-.79 2.05-1.33 4.24-1.95 7.74-2.44l.01.01ZM443.67 72.67c-.4-2.2-1.15-4.33-2.37-6.22-1.49-2.32-3.58-4.19-5.91-5.64-6.17-3.81-13.75-5.11-20.83-6.01-3.23-.41-6.47-.69-9.72-.92l-1.39-.12c-.85-.07-1.52-.1-2.05-.1-1.08-.06-2.17-.12-3.25-.17-.08 0-.14.02-.19.05-.1.05-.18.14-.16.3.27 2.55-.01 5.12-.92 7.52-.15.38.4.56.62.28 1.32.59 2.68 1.05 4.08 1.37 0 2.78-.14 7.58-.33 12.91 0 0 0 .02-.01.03-.61 3.66-.79 7.42-1 11.12-.23 4.01-.43 8.03-.44 12.05 0 .64 0 1.28.03 1.93.02.31 0 .68.15.96.06.11.14.16.24.17-.2.17-.21.54.11.59 3.83.67 7.78.71 11.68.25 2.3-.19 4.87-.65 7.65-1.56 1.85-.54 3.67-1.18 5.43-1.91 7.2-3.02 14.31-8.07 17.35-15.53.76-1.86 1.17-3.8 1.31-5.75.3-1.93.28-3.82-.09-5.58l.01-.02Zm-19.32-15.42c5.74 1.41 11.94 3.68 15.65 8.55.25.32.47.65.69.99-2.3-2.82-5.68-5.69-12.88-8.23-2.16-.76-4.35-1.43-6.64-2.02 1.06.21 2.13.45 3.18.71Zm-25.82-3.04c.13 0 .27.01.4.02-.14.1-.26.23-.37.38 0-.13-.02-.26-.03-.4Zm34.82 22.17c-.75 3.09-3.55 5.66-5.88 7.58-3.35 2.76-7.21 5.03-11.28 6.54-1.33.49-2.71.9-4.12 1.15.06-1.38.08-2.76.07-4.13-.02-3.78-.16-7.56-.41-11.33-.09-1.37-.18-2.74-.37-4.1 0-.06-.03-.11-.06-.15.09-3.25.12-6.16.03-8.12 6.86 1.05 10.56 2.17 14.06 3.62 5.52 2.28 8.59 5.44 7.97 8.96l-.01-.02Zm-22 16.15c-.12 0-.23-.02-.34-.03l.34-.03v.06Zm-.69-.7c0-3.13.26-8.84.47-14.51.06 1.2.11 2.41.15 3.6.15 3.6.25 7.23.09 10.83-.24.03-.48.05-.71.07v.01Zm-12.33-30.94c.37.63 2.01 1.01 3.23 1.25v.15c-1.31-.31-2.59-.73-3.83-1.29.12-.36.23-.72.33-1.09.08.48.18.84.27.98Zm13.7 31.65v-.18c3.41-.56 6.71-2.02 9.69-3.68 2.31-1.28 4.59-2.78 6.63-4.53-4.69 4.53-11.61 8.24-16.33 8.38l.01.01Zm24.07-.75c-2.05 1.93-4.37 3.56-6.83 4.95 2.7-1.78 5.52-4.03 8.42-6.87.82-.82 1.56-1.69 2.23-2.59-1.08 1.65-2.38 3.16-3.81 4.51h-.01ZM187.16 92.14c-.79-2.47-2.1-7.12-3.1-6.87-.19-.01-2.09.77-4.08 1.54-3.06 1.18-5.91 2.13-10.09 2.82-2.74.42-5.87 1.01-10.61 1.06.04-3.34.05-6.01.05-7.99 7.97-.65 12.33-2.11 16.37-3.55 1.11-.39 2.69-1.01 2.63-1.8-.08-.35-.55-1.39-1.17-2.61-.47-1.16-.98-2.31-1.61-3.38-.42-.71-1.04-1.69-1.86-2.06-.11-.08-.22-.13-.29-.12-.02 0-.04 0-.07.01-.19-.04-.39-.05-.6-.01-.17.03-.24.15-.25.28-.04.02-.09.04-.14.05-4.33 1.48-8.85 2.33-13.24 3.61a499.1 499.1 0 0 0-.31-8.19c4.51-.99 8.88-1.38 13.11-1.82 3.68-.38 6.28.12 7.47.34.59.11.9.16 1.16.18h.1c-.1.37.44.66.62.28.02-.04.03-.08.05-.13.15.2.53.22.62-.1.17-.58.19-1.21.21-1.81v-.36c.03-.15.05-.3.07-.45.52-2.47.33-5.09-.64-7.44-.11-.27-.44-.28-.6-.14-.08-.21-.15-.42-.24-.62-.19-.41-.79-.05-.6.35.03.07.05.15.09.22-.98-.42-2.15-.54-3.17-.63-2.17-.19-4.37-.14-6.54 0-5.7.35-11.4 1.3-16.91 2.79-2.08.56-4.13 1.22-6.14 2-4.54 1.05-3.79 1.51-2.17 6.07.18.51.46 1.68.54 1.94.82 2.47 1.08 2.13 3.1 2.13s0 .05 0 .08h.52c-.48 2.66-.51 5.45-.62 8.13-.15 3.48-.22 6.96-.28 10.45 0 .41-.01.82-.02 1.23-.16.29-.33.57-.51.85-.05.38-.09.77-.14 1.18-.42 3.52-.59 6.48-.52 8.8v.34c.02.47.05.76.06.87.16 1.57-.26 3.47 1.35 3.79 1.61.32 3.5.55 4.85.55.11 0 .22-.02.33-.02 1.79.24 3.67.05 5.45-.12 2.85-.28 5.69-.7 8.51-1.19 3.03-.53 6.05-1.14 9.04-1.86 2.4-.58 4.82-1.19 7.13-2.06.51-.19 1.73-.57 2.46-1.14 1.81-.68 2.18-1 1.57-2.67-.23-.62-.48-1.49-.91-2.78l-.03-.02Zm-11.12-38.71c.89.05 1.93.08 2.89.3-.33 0-.68-.02-1.06-.03-8.28-.26-14.88.75-23.97 2.51 2.41-.64 4.85-1.16 7.28-1.59 4.87-.86 9.91-1.45 14.86-1.19Zm-26.53 22.13c.03 1.71.04 3.43 0 5.14-.04 1.27-.11 2.55-.24 3.82 0-.73.02-1.46.04-2.19.05-2.26.12-4.51.22-6.77h-.02Zm6.73 27.85c.2-.1.4-.21.58-.33 1.82-.17 3.82-.24 5.94-.34-.86.11-1.72.24-2.58.33-1.27.14-2.61.31-3.93.34h-.01ZM534.48 85.44c-3.52-8.38-7.07-16.75-10.5-25.17-.63-1.54-1.25-3.09-1.86-4.65-.31-.8-.65-1.6-.87-2.43-.04-.17-.17-.24-.31-.25.1-.2 0-.51-.29-.53-1.59-.08-3.18-.22-4.78-.25-1.96-.03-3.91.13-5.84.42-.31.05-.31.38-.13.56-.03.06-.05.14-.04.22.23 1.54.63 3.06 1.16 4.53.13.35.27.7.41 1.06l-2.68 6.18c-.11.03-.2.09-.25.22-.67 1.9-1.52 3.73-2.34 5.56a536.85 536.85 0 0 1-3.9 8.45c-2.64 5.64-5.34 11.25-7.91 16.93-.44.97-.88 1.94-1.29 2.93-.2.48-.47 1-.55 1.52v.05c-.02.12.02.26.16.34 1.19.73 2.41 1.41 3.66 2.05 1.2.62 2.45 1.25 3.76 1.61.43.12.62-.55.19-.67-1.13-.31-2.2-.83-3.24-1.36 1.09.36 2.1.69 2.75.93 2.82 1.01 2.38 1.1 4.3-3.75 2.1-1.09 4.34-1.96 6.53-2.79 4.35-1.64 8.8-3.03 13.27-4.29.82 2.01 1.77 3.97 2.72 5.92.35.83.62 1.45.79 1.82.22.42.45.8.69 1.15.17.33.33.67.5 1 .42.8.84 1.63 1.4 2.35.23.29.6 0 .55-.31 1.53-.02 3.06-.07 4.58-.27.92-.12 1.82-.32 2.71-.54 1.39-.27 3.85-1.11 3.74-1.42-.67-1.96-1.55-3.87-2.34-5.78-1.57-3.78-3.16-7.56-4.75-11.33v-.01Zm-11.65-26.16c1.54 3.81 3.12 7.6 4.7 11.4 2.94 7.05 5.91 14.09 8.87 21.13l-1.06-2.17c-2.71-5.51-5.2-11.19-7.41-16.87l-6.65-17.15c-.65-1.45-.55-2.19-.93-2.53.09 0 .18.01.28.02a.29.29 0 0 0-.04.26c.52 2.02 1.47 3.98 2.25 5.91h-.01Zm-6.58 13.58c.05-.15.09-.31.14-.46 1.41 3.92 2.88 7.9 4.39 11.87-3.22.52-6.38 1.25-9.46 2.14.55-1.22 1.05-2.46 1.53-3.7 1.24-3.24 2.37-6.53 3.39-9.85h.01Zm-.23-20c.36 0 .73.03 1.09.05-2.15.1-5.18.33-5.87.74-.24.15-.41.3-.53.45-.06-.29-.13-.58-.18-.88 1.82-.26 3.65-.39 5.49-.35v-.01Zm-.09 18.72c-.49 1.67-1.05 3.33-1.6 4.97-1.07 3.19-2.19 6.38-3.57 9.46-.09.21-.19.43-.29.65-.25.07-.5.14-.74.22 2.53-6.16 4.61-11.29 6.2-15.3Zm-6.34 25.16c4.97-2.38 9.37-4.1 14.02-5.27l.26.64c-4.8 1.35-9.63 2.8-14.28 4.63Zm20.17 6.76c.33.23.68.42 1.04.56h-.33c-.12 0-.21.06-.26.13-.15-.23-.31-.45-.45-.7v.01ZM226.57 91.75c-3.55-4.74-6.68-9.11-9.31-12.99 9.2-15.25 10.05-17.81 10.35-18.38.17-.34 1.09-2.27.64-2.53-1.13-.65-1.03-.65-2.97-1.71-1.19-.65-3.04-1.61-4.53-2.12-1.71-.59-1.24-.36-3 2.77-.06.1-.11.2-.17.3-.75 1.02-1.48 2.05-2.2 3.09-1.88 2.71-3.73 5.45-5.69 8.1-3.68-4.91-6.88-8.76-9.51-11.43-.15-.15-.3-.29-.46-.42-1.27-1.28-7.24 3.53-7.93 5.58-.09.09-.19.16-.28.25-.27.26.03.64.33.58.19.65.5 1.29.94 1.91 3.85 5.06 7.19 9.76 9.94 14-1.23 2.61-3.06 5-4.67 7.38l-2.28 3.33c-.5.66-.93 1.23-1.29 1.69-.67.93-2.09 2.61-2.3 3.87-.51.85-1.16 1.84-1.29 2.83-.06.44.61.63.67.19.01-.08.04-.15.06-.22 1.36 1.08 2.76 2.11 4.19 3.11 1.3.91 2.62 1.85 4.04 2.56.21.1.4 0 .48-.17.24.07.48.14.72.2.44.1.62-.57.19-.67-2.02-.48-3.77-1.57-5.23-3.02-.47-.46-.9-.96-1.32-1.46 1.74 1.35 4.2 2.89 5.89 4.14 1.39 1.03 2.85-2.27 4.22-4.2 1.86-2.64 3.96-5.86 5.52-8.29l10.39 14.51c.67.81 1.14 1.21 1.57 1.36-.05.24.12.51.41.4 1.53-.58 3.05-1.19 4.54-1.87 1.52-.69 3.06-1.45 4.36-2.5a.28.28 0 0 0 .12-.23c1.66-1.1.81-1.74-1.41-4.91-1.13-1.58-1.71-2.36-3.7-5.01l-.03-.02Zm2.41 6.54c.56 1.15 1.19 2.52 1.11 3.81-.06.04-.12.07-.17.1-.03-.88-.55-2.66-.94-3.91Zm-16.51-32.73c1.86-2.65 3.65-5.35 5.57-7.95.4-.55.81-1.13 1.26-1.66.19-.18.38-.33.56-.45.18.03.36.08.55.13l-8.05 10.11.12-.18h-.01ZM192.7 95.48c.79-1.37 1.66-2.69 2.54-4 1.19-1.79 2.4-3.56 3.61-5.33-.04.09-.09.17-.13.26-.1.22.03.41.2.49-2.47 3.42-4.89 6.73-6.4 9.28.21.24.4.48.63.75-.24.07-.4.36-.17.56.4.33.72.77 1.05 1.17.09.11.18.21.27.32-.84-.61-1.66-1.24-2.47-1.88.24-.57.58-1.11.87-1.61v-.01Zm7.46-10.32c.47-.81.98-1.59 1.49-2.37.31-.48.64-.95.96-1.43.26-.29.52-.56.75-.79-.99 1.48-2.09 3.03-3.2 4.59Zm10.03-16.22s-.03-.05-.05-.07c.22-.29.43-.59.64-.89-.2.32-.4.65-.58.96h-.01ZM371.54 87.96c-.01-.08-.01-.16-.03-.23-.06-.38-.58-.29-.66.03-.3-.05-.6-.08-.81-.11-1.14-.15-2.29-.19-3.44-.2 1.04-.09 2.09-.18 3.14-.23.45-.02.45-.72 0-.7-6.57.35-13.14 1.23-19.65 2.11-1.53.21-3.05.42-4.57.68-.01 0-.02.01-.04.01-.04-3.33-.13-6.66-.24-9.99-.19-5.7-.4-11.41-.88-17.1-.13-1.51-.23-3.07-.49-4.58 0-.25 0-.48-.02-.68-.06-1.19-.04-2.61-.68-2.78-.16-.07-.72-.16-1.5-.24.22-.17.16-.62-.2-.63-1.19-.04-2.39.09-3.57.23-1.2.14-2.41.32-3.59.6-.16-.1-.41-.06-.5.12-.06.02-.13.03-.19.05-.35.1-.29.55-.03.66-.26.6-.19 2.27-.21 3-.02.66-.66 33.73-.9 40.3-.03.65.06 1.12.04 1.45-.16 3.05.87 4.96 6.34 3.93 1.09-.08 2.75-.77 5.36-1.43 4.13-1.04 5.78-1.52 6.2-1.65 6.43-1.69 6.78-1.97 11.72-2.43.55-.05 4.8-.38 6.03-.3.64.04 1.19.07 1.65.1.09 0 .16-.03.24-.05.1.27.56.33.66-.02.39-1.32.61-2.71.78-4.08.2-1.61.29-3.24.15-4.86.24.03.52-.23.38-.53-.09-.2-.27-.33-.49-.43v-.02Zm-.63.56c.07.57.11 1.14.11 1.71-.21-.99-.53-1.71-.95-1.87.22.03.44.06.65.11.06.01.12.04.19.05Zm-25.41 1.73c1.54-.36 3.1-.64 4.66-.89-1.61.37-3.18.77-4.66 1.2v-.31Zm-.86-7.37c-.07-1.37-.16-2.75-.25-4.12-.21-3.13-.45-6.27-.79-9.4.02-2.25.08-4.31.13-6.11.16 2.08.29 4.16.4 6.24.23 4.46.38 8.93.5 13.39h.01Zm-.94-4c.16 2.41.29 4.83.39 7.24.06 1.6.14 3.22.09 4.83-.15.05-.32.09-.47.14V78.88h-.01ZM483.72 92.83c-3.05-2.28-6.22-4.4-9.38-6.51 8.86-6.49 13.49-12.95 13.73-19.23.04-.76 0-1.5-.13-2.2-.67-3.82-3.5-6.68-8.39-8.48.13.04.27.08.4.13 3.92 1.39 7.74 4.23 8.5 8.56.34 1.95-.05 3.96-.98 5.69-.21.4.39.75.6.35 1.86-3.46 1.46-7.55-.97-10.63-3.53-4.47-9.76-5.88-15.16-6.16-2.32-.12-4.64-.04-6.95.19-6 .32-12.71 1.68-17.63 3.21-.37.11-.67.23-.92.35-.2-.17-.62.02-.57.37v.03c-.64.68-.18 1.64.48 3.21.38.91.67 1.89 1.15 2.58.32.76.68 1.51 1.13 2.19.14.21.38.19.53.07.19-.02.38-.05.57-.08v1.57c-.06.06-.1.13-.11.23-.27 4.18-.34 8.38-.48 12.57l-.3 9.03c-.24 3.91-.44 6.77-.46 7.26-.05.88-.11 1.95.07 2.81-.01.22-.02.43-.04.65 0 .11-.02.23-.03.35 0 .05-.03.27-.01.16-.05.4.5.59.64.28.05.04.12.08.2.08 1.75.13 3.5.28 5.25.3 1.69.02 3.38-.12 5.06-.32.08.23.36.39.55.15.06-.08.11-.17.16-.26.18-.09.24-.32.18-.48.05-.2.1-.4.13-.6.16-.86.25-1.74.33-2.62.11-1.17.17-2.34.23-3.51.15-.01.32-.03.52-.04.36-.03 1.73-.15 2.06-.15.39 0 .7-.02.95-.04 1.76 1.11 3.45 2.35 5.14 3.55 2.83 2.01 5.64 4.04 8.47 6.04 1.42 1 2.85 2 4.29 2.97.1.06.19.07.27.04.08 0 .17-.02.25-.1 1.61-1.56 3.15-3.18 4.6-4.88.75-.88 1.49-1.78 2.15-2.73.01.01.03.02.04.03.34.3.83-.2.49-.49-2.16-1.9-4.34-3.76-6.64-5.48l.03-.01Zm-6.38-3.65a55.72 55.72 0 0 0-4-2.13c.14-.1.26-.19.4-.29 1.2.81 2.4 1.61 3.6 2.42Zm-20.1 11.78c.67-.37 1.23-.91 1.67-1.6-.11.5-.24 1-.38 1.49-.43.04-.86.08-1.29.11Zm2.38-37.24c1.34-.31 2.56-.52 3.71-.69-1.03.19-2.04.41-3.04.65-.14-.07-.34-.02-.45.11-.07.02-.15.04-.22.05v-.13.01Zm.04.84c.07-.02.14-.03.2-.05.34 1.66.41 3.41.5 5.09.17 2.9.24 5.81.28 8.71l.03 3.17c-.17.07-.34.14-.51.2-.06-4.96-.21-10.58-.51-17.12h.01Zm16.04 5.62c-1.16 2.25-3.06 4.1-5.02 5.66-2.8 2.23-5.99 3.97-9.3 5.35-.01-3.56-.09-7.12-.27-10.67-.1-2.04-.16-4.16-.57-6.18 3.3-.78 6.72-1.36 10.1-1.1 1.85.14 4.23.59 5.32 2.29.92 1.43.46 3.24-.26 4.65Zm.85-.18c.6-1.37.9-2.92.28-4.32-.67-1.52-2.2-2.32-3.76-2.74.46.1.89.21 1.29.37 1.74.67 2.69 1.88 2.93 3.21.2 1.13-.05 2.25-.74 3.47V70Zm-27.47-4.14c-.12-.19-.23-.38-.34-.57.74.42.85.36.99.41v.08c-.22.03-.43.06-.65.08Zm11.21 30.46c-.08 1.08-.16 2.17-.33 3.24-.05.35-.11.69-.2 1.03 0 .04-.02.07-.03.11-.15.02-.3.04-.45.05.45-1.64.76-3.36.79-5.07.03-.29.08-.57.1-.89-.03-.31-.03-.47.24-.57-.04.69-.07 1.39-.12 2.08v.02Zm5.6-2.47c.48.11.92.52 2.49 1.72-.46-.32-.92-.65-1.38-.97-.37-.25-.73-.5-1.1-.75h-.01Zm21.23 7.24a70.76 70.76 0 0 1-4.37 4.63c-.14-.09-.27-.19-.4-.28.19-.09.37-.24.55-.47.87-1.14 5.43-5.51 5.49-7.45.31.26.62.53.92.79-.67.97-1.42 1.88-2.19 2.77v.01Z",
fill: "currentColor",
transform: "translate(-144.023 -51.76)"
}
)
]
}
);
var ExcalidrawLogo = ({
style,
size = "small",
withText
}) => {
return /* @__PURE__ */ jsxs79("div", { className: `ExcalidrawLogo is-${size}`, style, children: [
/* @__PURE__ */ jsx149(LogoIcon, {}),
withText && /* @__PURE__ */ jsx149(LogoText, {})
] });
};
// components/welcome-screen/WelcomeScreen.Center.tsx
import { Fragment as Fragment24, jsx as jsx150, jsxs as jsxs80 } from "react/jsx-runtime";
var WelcomeScreenMenuItemContent = ({
icon,
shortcut,
children
}) => {
const device = useDevice();
return /* @__PURE__ */ jsxs80(Fragment24, { children: [
/* @__PURE__ */ jsx150("div", { className: "welcome-screen-menu-item__icon", children: icon }),
/* @__PURE__ */ jsx150("div", { className: "welcome-screen-menu-item__text", children }),
shortcut && !device.editor.isMobile && /* @__PURE__ */ jsx150("div", { className: "welcome-screen-menu-item__shortcut", children: shortcut })
] });
};
WelcomeScreenMenuItemContent.displayName = "WelcomeScreenMenuItemContent";
var WelcomeScreenMenuItem = ({
onSelect,
children,
icon,
shortcut,
className = "",
...props
}) => {
return /* @__PURE__ */ jsx150(
"button",
{
...props,
type: "button",
className: `welcome-screen-menu-item ${className}`,
onClick: onSelect,
children: /* @__PURE__ */ jsx150(WelcomeScreenMenuItemContent, { icon, shortcut, children })
}
);
};
WelcomeScreenMenuItem.displayName = "WelcomeScreenMenuItem";
var WelcomeScreenMenuItemLink = ({
children,
href,
icon,
shortcut,
className = "",
...props
}) => {
return /* @__PURE__ */ jsx150(
"a",
{
...props,
className: `welcome-screen-menu-item ${className}`,
href,
target: "_blank",
rel: "noreferrer",
children: /* @__PURE__ */ jsx150(WelcomeScreenMenuItemContent, { icon, shortcut, children })
}
);
};
WelcomeScreenMenuItemLink.displayName = "WelcomeScreenMenuItemLink";
var Center = ({ children }) => {
const { WelcomeScreenCenterTunnel } = useTunnels();
return /* @__PURE__ */ jsx150(WelcomeScreenCenterTunnel.In, { children: /* @__PURE__ */ jsx150("div", { className: "welcome-screen-center", children: children || /* @__PURE__ */ jsxs80(Fragment24, { children: [
/* @__PURE__ */ jsx150(Logo, {}),
/* @__PURE__ */ jsx150(Heading, { children: t("welcomeScreen.defaults.center_heading") }),
/* @__PURE__ */ jsxs80(Menu, { children: [
/* @__PURE__ */ jsx150(MenuItemLoadScene, {}),
/* @__PURE__ */ jsx150(MenuItemHelp, {})
] })
] }) }) });
};
Center.displayName = "Center";
var Logo = ({ children }) => {
return /* @__PURE__ */ jsx150("div", { className: "welcome-screen-center__logo excalifont welcome-screen-decor", children: children || /* @__PURE__ */ jsx150(ExcalidrawLogo, { withText: true }) });
};
Logo.displayName = "Logo";
var Heading = ({ children }) => {
return /* @__PURE__ */ jsx150("div", { className: "welcome-screen-center__heading welcome-screen-decor excalifont", children });
};
Heading.displayName = "Heading";
var Menu = ({ children }) => {
return /* @__PURE__ */ jsx150("div", { className: "welcome-screen-menu", children });
};
Menu.displayName = "Menu";
var MenuItemHelp = () => {
const actionManager = useExcalidrawActionManager();
return /* @__PURE__ */ jsx150(
WelcomeScreenMenuItem,
{
onSelect: () => actionManager.executeAction(actionShortcuts),
shortcut: "?",
icon: HelpIcon,
children: t("helpDialog.title")
}
);
};
MenuItemHelp.displayName = "MenuItemHelp";
var MenuItemLoadScene = () => {
const appState = useUIAppState();
const actionManager = useExcalidrawActionManager();
if (appState.viewModeEnabled) {
return null;
}
return /* @__PURE__ */ jsx150(
WelcomeScreenMenuItem,
{
onSelect: () => actionManager.executeAction(actionLoadScene),
shortcut: getShortcutFromShortcutName("loadScene"),
icon: LoadIcon,
children: t("buttons.load")
}
);
};
MenuItemLoadScene.displayName = "MenuItemLoadScene";
var MenuItemLiveCollaborationTrigger = ({
onSelect
}) => {
const { t: t2 } = useI18n();
return /* @__PURE__ */ jsx150(WelcomeScreenMenuItem, { shortcut: null, onSelect, icon: usersIcon, children: t2("labels.liveCollaboration") });
};
MenuItemLiveCollaborationTrigger.displayName = "MenuItemLiveCollaborationTrigger";
Center.Logo = Logo;
Center.Heading = Heading;
Center.Menu = Menu;
Center.MenuItem = WelcomeScreenMenuItem;
Center.MenuItemLink = WelcomeScreenMenuItemLink;
Center.MenuItemHelp = MenuItemHelp;
Center.MenuItemLoadScene = MenuItemLoadScene;
Center.MenuItemLiveCollaborationTrigger = MenuItemLiveCollaborationTrigger;
// components/welcome-screen/WelcomeScreen.Hints.tsx
import { jsx as jsx151, jsxs as jsxs81 } from "react/jsx-runtime";
var MenuHint = ({ children }) => {
const { WelcomeScreenMenuHintTunnel } = useTunnels();
return /* @__PURE__ */ jsx151(WelcomeScreenMenuHintTunnel.In, { children: /* @__PURE__ */ jsxs81("div", { className: "excalifont welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--menu", children: [
WelcomeScreenMenuArrow,
/* @__PURE__ */ jsx151("div", { className: "welcome-screen-decor-hint__label", children: children || t("welcomeScreen.defaults.menuHint") })
] }) });
};
MenuHint.displayName = "MenuHint";
var ToolbarHint = ({ children }) => {
const { WelcomeScreenToolbarHintTunnel } = useTunnels();
return /* @__PURE__ */ jsx151(WelcomeScreenToolbarHintTunnel.In, { children: /* @__PURE__ */ jsxs81("div", { className: "excalifont welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--toolbar", children: [
/* @__PURE__ */ jsx151("div", { className: "welcome-screen-decor-hint__label", children: children || t("welcomeScreen.defaults.toolbarHint") }),
WelcomeScreenTopToolbarArrow
] }) });
};
ToolbarHint.displayName = "ToolbarHint";
var HelpHint = ({ children }) => {
const { WelcomeScreenHelpHintTunnel } = useTunnels();
return /* @__PURE__ */ jsx151(WelcomeScreenHelpHintTunnel.In, { children: /* @__PURE__ */ jsxs81("div", { className: "excalifont welcome-screen-decor welcome-screen-decor-hint welcome-screen-decor-hint--help", children: [
/* @__PURE__ */ jsx151("div", { children: children || t("welcomeScreen.defaults.helpHint") }),
WelcomeScreenHelpArrow
] }) });
};
HelpHint.displayName = "HelpHint";
// components/welcome-screen/WelcomeScreen.tsx
import { Fragment as Fragment25, jsx as jsx152, jsxs as jsxs82 } from "react/jsx-runtime";
var WelcomeScreen = (props) => {
return /* @__PURE__ */ jsx152(Fragment25, { children: props.children || /* @__PURE__ */ jsxs82(Fragment25, { children: [
/* @__PURE__ */ jsx152(Center, {}),
/* @__PURE__ */ jsx152(MenuHint, {}),
/* @__PURE__ */ jsx152(ToolbarHint, {}),
/* @__PURE__ */ jsx152(HelpHint, {})
] }) });
};
WelcomeScreen.displayName = "WelcomeScreen";
WelcomeScreen.Center = Center;
WelcomeScreen.Hints = { MenuHint, ToolbarHint, HelpHint };
var WelcomeScreen_default = WelcomeScreen;
// components/live-collaboration/LiveCollaborationTrigger.tsx
import clsx57 from "clsx";
import { jsx as jsx153, jsxs as jsxs83 } from "react/jsx-runtime";
var LiveCollaborationTrigger2 = ({
isCollaborating,
onSelect,
...rest
}) => {
const appState = useUIAppState();
const showIconOnly = appState.width < 830;
return /* @__PURE__ */ jsxs83(
Button,
{
...rest,
className: clsx57("collab-button", { active: isCollaborating }),
type: "button",
onSelect,
style: { position: "relative", width: showIconOnly ? void 0 : "auto" },
title: t("labels.liveCollaboration"),
children: [
showIconOnly ? share : t("labels.share"),
appState.collaborators.size > 0 && /* @__PURE__ */ jsx153("div", { className: "CollabButton-collaborators", children: appState.collaborators.size })
]
}
);
};
var LiveCollaborationTrigger_default = LiveCollaborationTrigger2;
LiveCollaborationTrigger2.displayName = "LiveCollaborationTrigger";
// data/reconcile.ts
import throttle4 from "lodash.throttle";
var shouldDiscardRemoteElement = (localAppState, local, remote) => {
if (local && // local element is being edited
(local.id === localAppState.editingTextElement?.id || local.id === localAppState.resizingElement?.id || local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array
// local element is newer
local.version > remote.version || // resolve conflicting edits deterministically by taking the one with
// the lowest versionNonce
local.version === remote.version && local.versionNonce < remote.versionNonce)) {
return true;
}
return false;
};
var validateIndicesThrottled = throttle4(
(orderedElements, localElements, remoteElements) => {
if (define_import_meta_env_default.DEV || define_import_meta_env_default.MODE === ENV.TEST || window?.DEBUG_FRACTIONAL_INDICES) {
const elements = syncInvalidIndices(
orderedElements.map((x) => ({ ...x }))
);
validateFractionalIndices(elements, {
// throw in dev & test only, to remain functional on `DEBUG_FRACTIONAL_INDICES`
shouldThrow: define_import_meta_env_default.DEV || define_import_meta_env_default.MODE === ENV.TEST,
includeBoundTextValidation: true,
reconciliationContext: {
localElements,
remoteElements
}
});
}
},
1e3 * 60,
{ leading: true, trailing: false }
);
var reconcileElements = (localElements, remoteElements, localAppState) => {
const localElementsMap = arrayToMap(localElements);
const reconciledElements = [];
const added = /* @__PURE__ */ new Set();
for (const remoteElement of remoteElements) {
if (!added.has(remoteElement.id)) {
const localElement = localElementsMap.get(remoteElement.id);
const discardRemoteElement = shouldDiscardRemoteElement(
localAppState,
localElement,
remoteElement
);
if (localElement && discardRemoteElement) {
reconciledElements.push(localElement);
added.add(localElement.id);
} else {
reconciledElements.push(remoteElement);
added.add(remoteElement.id);
}
}
}
for (const localElement of localElements) {
if (!added.has(localElement.id)) {
reconciledElements.push(localElement);
added.add(localElement.id);
}
}
const orderedElements = orderByFractionalIndex(reconciledElements);
validateIndicesThrottled(orderedElements, localElements, remoteElements);
syncInvalidIndices(orderedElements);
return orderedElements;
};
// components/TTDDialog/TTDDialogTrigger.tsx
import { jsx as jsx154, jsxs as jsxs84 } from "react/jsx-runtime";
var TTDDialogTrigger = ({
children,
icon
}) => {
const { TTDDialogTriggerTunnel } = useTunnels();
const setAppState = useExcalidrawSetAppState();
return /* @__PURE__ */ jsx154(TTDDialogTriggerTunnel.In, { children: /* @__PURE__ */ jsxs84(
DropdownMenu_default.Item,
{
onSelect: () => {
trackEvent("ai", "dialog open", "ttd");
setAppState({ openDialog: { name: "ttd", tab: "text-to-diagram" } });
},
icon: icon ?? brainIcon,
children: [
children ?? t("labels.textToDiagram"),
/* @__PURE__ */ jsx154(DropdownMenu_default.Item.Badge, { children: "AI" })
]
}
) });
};
TTDDialogTrigger.displayName = "TTDDialogTrigger";
// components/DiagramToCodePlugin/DiagramToCodePlugin.tsx
import { useLayoutEffect as useLayoutEffect9 } from "react";
var DiagramToCodePlugin = (props) => {
const app = useApp();
useLayoutEffect9(() => {
app.setPlugins({
diagramToCode: { generate: props.generate }
});
}, [app, props.generate]);
return null;
};
// index.tsx
import { jsx as jsx155 } from "react/jsx-runtime";
polyfill_default();
var ExcalidrawBase = (props) => {
const {
onChange,
initialData,
excalidrawAPI,
isCollaborating = false,
onPointerUpdate,
renderTopRightUI,
langCode = defaultLang.code,
viewModeEnabled,
zenModeEnabled,
gridModeEnabled,
libraryReturnUrl,
theme,
name,
renderCustomStats,
onPaste,
detectScroll = true,
handleKeyboardGlobally = false,
onLibraryChange,
autoFocus = false,
generateIdForFile,
onLinkOpen,
generateLinkForSelection,
onPointerDown,
onPointerUp,
onScrollChange,
onDuplicate,
children,
validateEmbeddable,
renderEmbeddable,
aiEnabled,
showDeprecatedFonts
} = props;
const canvasActions = props.UIOptions?.canvasActions;
const UIOptions = {
...props.UIOptions,
canvasActions: {
...DEFAULT_UI_OPTIONS.canvasActions,
...canvasActions
},
tools: {
image: props.UIOptions?.tools?.image ?? true
}
};
if (canvasActions?.export) {
UIOptions.canvasActions.export.saveFileToDisk = canvasActions.export?.saveFileToDisk ?? DEFAULT_UI_OPTIONS.canvasActions.export.saveFileToDisk;
}
if (UIOptions.canvasActions.toggleTheme === null && typeof theme === "undefined") {
UIOptions.canvasActions.toggleTheme = true;
}
useEffect45(() => {
const importPolyfill = async () => {
await import("canvas-roundrect-polyfill");
};
importPolyfill();
const handleTouchMove = (event) => {
if (typeof event.scale === "number" && event.scale !== 1) {
event.preventDefault();
}
};
document.addEventListener("touchmove", handleTouchMove, {
passive: false
});
return () => {
document.removeEventListener("touchmove", handleTouchMove);
};
}, []);
return /* @__PURE__ */ jsx155(EditorJotaiProvider, { store: editorJotaiStore, children: /* @__PURE__ */ jsx155(InitializeApp, { langCode, theme, children: /* @__PURE__ */ jsx155(
App_default,
{
onChange,
initialData,
excalidrawAPI,
isCollaborating,
onPointerUpdate,
renderTopRightUI,
langCode,
viewModeEnabled,
zenModeEnabled,
gridModeEnabled,
libraryReturnUrl,
theme,
name,
renderCustomStats,
UIOptions,
onPaste,
detectScroll,
handleKeyboardGlobally,
onLibraryChange,
autoFocus,
generateIdForFile,
onLinkOpen,
generateLinkForSelection,
onPointerDown,
onPointerUp,
onScrollChange,
onDuplicate,
validateEmbeddable,
renderEmbeddable,
aiEnabled: aiEnabled !== false,
showDeprecatedFonts,
children
}
) }) });
};
var areEqual5 = (prevProps, nextProps) => {
if (prevProps.children !== nextProps.children) {
return false;
}
const {
initialData: prevInitialData,
UIOptions: prevUIOptions = {},
...prev
} = prevProps;
const {
initialData: nextInitialData,
UIOptions: nextUIOptions = {},
...next
} = nextProps;
const prevUIOptionsKeys = Object.keys(prevUIOptions);
const nextUIOptionsKeys = Object.keys(nextUIOptions);
if (prevUIOptionsKeys.length !== nextUIOptionsKeys.length) {
return false;
}
const isUIOptionsSame = prevUIOptionsKeys.every((key) => {
if (key === "canvasActions") {
const canvasOptionKeys = Object.keys(
prevUIOptions.canvasActions
);
return canvasOptionKeys.every((key2) => {
if (key2 === "export" && prevUIOptions?.canvasActions?.export && nextUIOptions?.canvasActions?.export) {
return prevUIOptions.canvasActions.export.saveFileToDisk === nextUIOptions.canvasActions.export.saveFileToDisk;
}
return prevUIOptions?.canvasActions?.[key2] === nextUIOptions?.canvasActions?.[key2];
});
}
return prevUIOptions[key] === nextUIOptions[key];
});
return isUIOptionsSame && isShallowEqual(prev, next);
};
var Excalidraw = React44.memo(ExcalidrawBase, areEqual5);
Excalidraw.displayName = "Excalidraw";
export {
Button,
CaptureUpdateAction,
DEFAULT_LASER_COLOR,
DefaultSidebar,
DiagramToCodePlugin,
Excalidraw,
FONT_FAMILY,
FooterCenter_default as Footer,
LiveCollaborationTrigger_default as LiveCollaborationTrigger,
MIME_TYPES,
MainMenu_default as MainMenu,
ROUNDNESS,
Sidebar,
Stats,
THEME,
TTDDialog,
TTDDialogTrigger,
UserIdleState,
WelcomeScreen_default as WelcomeScreen,
bumpVersion,
convertToExcalidrawElements,
defaultLang,
elementPartiallyOverlapsWithOrContainsBBox,
elementsOverlappingBBox,
exportToBlob,
exportToCanvas2 as exportToCanvas,
exportToClipboard,
exportToSvg2 as exportToSvg,
getCommonBounds,
getDataURL,
getFreeDrawSvgPath,
getLibraryItemsHash,
getNonDeletedElements,
getSceneVersion,
getTextFromElements,
getVisibleSceneBounds,
hashElementsVersion,
hashString,
isElementInsideBBox,
isElementLink,
isInvisiblySmallElement,
isLinearElement,
languages,
loadFromBlob,
loadLibraryFromBlob,
loadSceneOrLibraryFromBlob,
mergeLibraryItems,
mutateElement,
newElementWith,
normalizeLink,
parseLibraryTokensFromUrl,
reconcileElements,
restore,
restoreAppState,
restoreElements,
restoreLibraryItems,
sceneCoordsToViewportCoords,
serializeAsJSON,
serializeLibraryAsJSON,
setCustomTextMetricsProvider,
useDevice,
useHandleLibrary,
useI18n,
viewportCoordsToSceneCoords,
zoomToFitBounds
};
//# sourceMappingURL=index.js.map