33142 lines
1.1 MiB
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
|