mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: in canvas links between shapes (#8812)
Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
This commit is contained in:
parent
a758aaf8f6
commit
c0b80a03bd
32 changed files with 1281 additions and 217 deletions
|
@ -87,7 +87,8 @@ export const actionClearCanvas = register({
|
|||
predicate: (elements, appState, props, app) => {
|
||||
return (
|
||||
!!app.props.UIOptions.canvasActions.clearCanvas &&
|
||||
!appState.viewModeEnabled
|
||||
!appState.viewModeEnabled &&
|
||||
appState.openDialog?.name !== "elementLinkSelector"
|
||||
);
|
||||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
|
|
105
packages/excalidraw/actions/actionElementLink.ts
Normal file
105
packages/excalidraw/actions/actionElementLink.ts
Normal file
|
@ -0,0 +1,105 @@
|
|||
import { copyTextToSystemClipboard } from "../clipboard";
|
||||
import { copyIcon, elementLinkIcon } from "../components/icons";
|
||||
import {
|
||||
canCreateLinkFromElements,
|
||||
defaultGetElementLinkFromSelection,
|
||||
getLinkIdAndTypeFromSelection,
|
||||
} from "../element/elementLink";
|
||||
import { t } from "../i18n";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { StoreAction } from "../store";
|
||||
import { register } from "./register";
|
||||
|
||||
export const 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,
|
||||
},
|
||||
},
|
||||
storeAction: StoreAction.NONE,
|
||||
};
|
||||
}
|
||||
return {
|
||||
appState,
|
||||
elements,
|
||||
app,
|
||||
storeAction: StoreAction.NONE,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return {
|
||||
appState,
|
||||
elements,
|
||||
app,
|
||||
storeAction: StoreAction.NONE,
|
||||
};
|
||||
},
|
||||
predicate: (elements, appState) =>
|
||||
canCreateLinkFromElements(getSelectedElements(elements, appState)),
|
||||
});
|
||||
|
||||
export const 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, storeAction: StoreAction.NONE };
|
||||
}
|
||||
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
openDialog: {
|
||||
name: "elementLinkSelector",
|
||||
sourceElementId: getSelectedElements(elements, appState)[0].id,
|
||||
},
|
||||
},
|
||||
storeAction: StoreAction.CAPTURE,
|
||||
};
|
||||
},
|
||||
predicate: (elements, appState, appProps, app) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
|
||||
return (
|
||||
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||
selectedElements.length === 1 &&
|
||||
canCreateLinkFromElements(selectedElements)
|
||||
);
|
||||
},
|
||||
trackEvent: false,
|
||||
});
|
|
@ -135,6 +135,8 @@ export type ActionName =
|
|||
| "autoResize"
|
||||
| "elementStats"
|
||||
| "searchMenu"
|
||||
| "copyElementLink"
|
||||
| "linkToElement"
|
||||
| "cropEditor";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
|
|
|
@ -84,6 +84,7 @@ export const getDefaultAppState = (): Omit<
|
|||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
selectedElementIds: {},
|
||||
hoveredElementIds: {},
|
||||
selectedGroupIds: {},
|
||||
selectedElementsAreBeingDragged: false,
|
||||
selectionElement: null,
|
||||
|
@ -210,6 +211,7 @@ const APP_STATE_STORAGE_CONF = (<
|
|||
scrollX: { browser: true, export: false, server: false },
|
||||
scrollY: { browser: true, export: false, server: false },
|
||||
selectedElementIds: { browser: true, export: false, server: false },
|
||||
hoveredElementIds: { browser: false, export: false, server: false },
|
||||
selectedGroupIds: { browser: true, export: false, server: false },
|
||||
selectedElementsAreBeingDragged: {
|
||||
browser: false,
|
||||
|
|
|
@ -49,7 +49,6 @@ import {
|
|||
} from "../appState";
|
||||
import type { PastedMixedContent } from "../clipboard";
|
||||
import { copyTextToSystemClipboard, parseClipboard } from "../clipboard";
|
||||
import { ARROW_TYPE, isSafari, type EXPORT_IMAGE_TYPES } from "../constants";
|
||||
import {
|
||||
APP_NAME,
|
||||
CURSOR_TYPE,
|
||||
|
@ -88,6 +87,10 @@ import {
|
|||
supportsResizeObserver,
|
||||
DEFAULT_COLLISION_THRESHOLD,
|
||||
DEFAULT_TEXT_ALIGN,
|
||||
ARROW_TYPE,
|
||||
DEFAULT_REDUCED_GLOBAL_ALPHA,
|
||||
isSafari,
|
||||
type EXPORT_IMAGE_TYPES,
|
||||
} from "../constants";
|
||||
import type { ExportedElements } from "../data";
|
||||
import { exportCanvas, loadFromBlob } from "../data";
|
||||
|
@ -461,6 +464,8 @@ import {
|
|||
} from "../../math";
|
||||
import { cropElement } from "../element/cropElement";
|
||||
import { wrapText } from "../element/textWrapping";
|
||||
import { actionCopyElementLink } from "../actions/actionElementLink";
|
||||
import { isElementLink, parseElementLinkFromURL } from "../element/elementLink";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
|
@ -1202,6 +1207,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
getContainingFrame(el, this.scene.getNonDeletedElementsMap()),
|
||||
this.elementsPendingErasure,
|
||||
null,
|
||||
this.state.openDialog?.name === "elementLinkSelector"
|
||||
? DEFAULT_REDUCED_GLOBAL_ALPHA
|
||||
: 1,
|
||||
),
|
||||
["--embeddable-radius" as string]: `${getCornerRadius(
|
||||
Math.min(el.width, el.height),
|
||||
|
@ -1520,7 +1528,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return (
|
||||
<div
|
||||
className={clsx("excalidraw excalidraw-container", {
|
||||
"excalidraw--view-mode": this.state.viewModeEnabled,
|
||||
"excalidraw--view-mode":
|
||||
this.state.viewModeEnabled ||
|
||||
this.state.openDialog?.name === "elementLinkSelector",
|
||||
"excalidraw--mobile": this.device.editor.isMobile,
|
||||
})}
|
||||
style={{
|
||||
|
@ -1579,6 +1589,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
app={this}
|
||||
isCollaborating={this.props.isCollaborating}
|
||||
generateLinkForSelection={
|
||||
this.props.generateLinkForSelection
|
||||
}
|
||||
>
|
||||
{this.props.children}
|
||||
</LayerUI>
|
||||
|
@ -1590,6 +1603,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
trails={[this.laserTrails, this.eraserTrail]}
|
||||
/>
|
||||
{selectedElements.length === 1 &&
|
||||
this.state.openDialog?.name !==
|
||||
"elementLinkSelector" &&
|
||||
this.state.showHyperlinkPopup && (
|
||||
<Hyperlink
|
||||
key={firstSelectedElement.id}
|
||||
|
@ -2325,6 +2340,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.fonts.loadSceneFonts().then((fontFaces) => {
|
||||
this.fonts.onLoaded(fontFaces);
|
||||
});
|
||||
|
||||
if (isElementLink(window.location.href)) {
|
||||
this.scrollToContent(window.location.href, { animate: false });
|
||||
}
|
||||
};
|
||||
|
||||
private isMobileBreakpoint = (width: number, height: number) => {
|
||||
|
@ -2761,6 +2780,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.deselectElements();
|
||||
}
|
||||
|
||||
// cleanup
|
||||
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 });
|
||||
}
|
||||
|
@ -3623,7 +3654,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||
private cancelInProgressAnimation: (() => void) | null = null;
|
||||
|
||||
scrollToContent = (
|
||||
/**
|
||||
* target to scroll to
|
||||
*
|
||||
* - string - id of element or group, or url containing elementLink
|
||||
* - ExcalidrawElement | ExcalidrawElement[] - element(s) objects
|
||||
*/
|
||||
target:
|
||||
| string
|
||||
| ExcalidrawElement
|
||||
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
||||
opts?: (
|
||||
|
@ -3650,6 +3688,34 @@ class App extends React.Component<AppProps, AppState> {
|
|||
canvasOffsets?: Offsets;
|
||||
},
|
||||
) => {
|
||||
if (typeof target === "string") {
|
||||
let id: string | null;
|
||||
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: 3000,
|
||||
closable: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.cancelInProgressAnimation?.();
|
||||
|
||||
// convert provided target into ExcalidrawElement[] if necessary
|
||||
|
@ -4214,6 +4280,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.state.openDialog?.name === "elementLinkSelector") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.actionManager.handleKeyDown(event)) {
|
||||
return;
|
||||
}
|
||||
|
@ -4485,7 +4555,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
||||
if (event.key === KEYS.SPACE) {
|
||||
if (this.state.viewModeEnabled) {
|
||||
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);
|
||||
|
@ -5372,18 +5445,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||
scenePointer: Readonly<{ x: number; y: number }>,
|
||||
hitElement: NonDeletedExcalidrawElement | null,
|
||||
): ExcalidrawElement | undefined => {
|
||||
// Reversing so we traverse the elements in decreasing order
|
||||
// of z-index
|
||||
const elements = this.scene.getNonDeletedElements().slice().reverse();
|
||||
let hitElementIndex = Infinity;
|
||||
const elements = this.scene.getNonDeletedElements();
|
||||
let hitElementIndex = -1;
|
||||
|
||||
return elements.find((element, index) => {
|
||||
for (let index = elements.length - 1; index >= 0; index--) {
|
||||
const element = elements[index];
|
||||
if (hitElement && element.id === hitElement.id) {
|
||||
hitElementIndex = index;
|
||||
}
|
||||
return (
|
||||
if (
|
||||
element.link &&
|
||||
index <= hitElementIndex &&
|
||||
index >= hitElementIndex &&
|
||||
isPointHittingLink(
|
||||
element,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
|
@ -5391,8 +5463,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
pointFrom(scenePointer.x, scenePointer.y),
|
||||
this.device.editor.isMobile,
|
||||
)
|
||||
);
|
||||
});
|
||||
) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private redirectToLink = (
|
||||
|
@ -5409,12 +5483,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.lastPointerUpEvent!.clientY,
|
||||
),
|
||||
);
|
||||
if (
|
||||
!this.hitLinkElement ||
|
||||
// For touch screen allow dragging threshold else strict check
|
||||
(isTouchScreen && draggedDistance > DRAGGING_THRESHOLD) ||
|
||||
(!isTouchScreen && draggedDistance !== 0)
|
||||
) {
|
||||
if (!this.hitLinkElement || draggedDistance > DRAGGING_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
const lastPointerDownCoords = viewportCoordsToSceneCoords(
|
||||
|
@ -5441,6 +5510,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.device.editor.isMobile,
|
||||
);
|
||||
if (lastPointerDownHittingLinkIcon && lastPointerUpHittingLinkIcon) {
|
||||
hideHyperlinkToolip();
|
||||
let url = this.hitLinkElement.link;
|
||||
if (url) {
|
||||
url = normalizeLink(url);
|
||||
|
@ -5827,6 +5897,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (
|
||||
(!this.state.selectedLinearElement ||
|
||||
this.state.selectedLinearElement.hoverPointIndex === -1) &&
|
||||
this.state.openDialog?.name !== "elementLinkSelector" &&
|
||||
!(selectedElements.length === 1 && isElbowArrow(selectedElements[0]))
|
||||
) {
|
||||
const elementWithTransformHandleType =
|
||||
|
@ -5851,7 +5922,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
}
|
||||
} else if (selectedElements.length > 1 && !isOverScrollBar) {
|
||||
} else if (
|
||||
selectedElements.length > 1 &&
|
||||
!isOverScrollBar &&
|
||||
this.state.openDialog?.name !== "elementLinkSelector"
|
||||
) {
|
||||
const transformHandleType = getTransformHandleTypeFromCoords(
|
||||
getCommonBounds(selectedElements),
|
||||
scenePointerX,
|
||||
|
@ -5910,6 +5985,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
);
|
||||
} 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) {
|
||||
|
@ -5955,6 +6032,32 @@ class App extends React.Component<AppProps, AppState> {
|
|||
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, {}),
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
private handleEraser = (
|
||||
|
@ -6212,7 +6315,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state,
|
||||
),
|
||||
},
|
||||
storeAction: StoreAction.UPDATE,
|
||||
storeAction:
|
||||
this.state.openDialog?.name === "elementLinkSelector"
|
||||
? StoreAction.NONE
|
||||
: StoreAction.UPDATE,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -6939,6 +7045,15 @@ class App extends React.Component<AppProps, AppState> {
|
|||
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
|
||||
|
@ -7032,7 +7147,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
|
||||
) {
|
||||
this.setState((prevState) => {
|
||||
const nextSelectedElementIds: { [id: string]: true } = {
|
||||
let nextSelectedElementIds: { [id: string]: true } = {
|
||||
...prevState.selectedElementIds,
|
||||
[hitElement.id]: true,
|
||||
};
|
||||
|
@ -7103,6 +7218,23 @@ class App extends React.Component<AppProps, AppState> {
|
|||
}
|
||||
}
|
||||
|
||||
// Finally, in shape selection mode, we'd like to
|
||||
// keep only one shape or group selected at a time.
|
||||
// This means, if the hitElement is a different shape or group
|
||||
// than the previously selected ones, we deselect the previous ones
|
||||
// and select the hitElement
|
||||
if (prevState.openDialog?.name === "elementLinkSelector") {
|
||||
if (
|
||||
!hitElement.groupIds.some(
|
||||
(gid) => prevState.selectedGroupIds[gid],
|
||||
)
|
||||
) {
|
||||
nextSelectedElementIds = {
|
||||
[hitElement.id]: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
|
@ -7747,6 +7879,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||
pointerDownState: PointerDownState,
|
||||
) {
|
||||
return withBatchedUpdatesThrottled((event: PointerEvent) => {
|
||||
if (this.state.openDialog?.name === "elementLinkSelector") {
|
||||
return;
|
||||
}
|
||||
const pointerCoords = viewportCoordsToSceneCoords(event, this.state);
|
||||
const lastPointerCoords =
|
||||
this.lastPointerMoveCoords ?? pointerDownState.origin;
|
||||
|
@ -10498,7 +10633,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
actionFlipVertical,
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionToggleLinearEditor,
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionLink,
|
||||
actionCopyElementLink,
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
actionDuplicateSelection,
|
||||
actionToggleElementLock,
|
||||
CONTEXT_MENU_SEPARATOR,
|
||||
|
|
|
@ -56,6 +56,10 @@ import { trackEvent } from "../../analytics";
|
|||
import { useStable } from "../../hooks/useStable";
|
||||
|
||||
import "./CommandPalette.scss";
|
||||
import {
|
||||
actionCopyElementLink,
|
||||
actionLinkToElement,
|
||||
} from "../../actions/actionElementLink";
|
||||
|
||||
const lastUsedPaletteItem = atom<CommandPaletteItem | null>(null);
|
||||
|
||||
|
@ -281,6 +285,8 @@ function CommandPaletteInner({
|
|||
actionManager.actions.toggleLinearEditor,
|
||||
actionManager.actions.cropEditor,
|
||||
actionLink,
|
||||
actionCopyElementLink,
|
||||
actionLinkToElement,
|
||||
].map((action: Action) =>
|
||||
actionToCommand(
|
||||
action,
|
||||
|
|
87
packages/excalidraw/components/ElementLinkDialog.scss
Normal file
87
packages/excalidraw/components/ElementLinkDialog.scss
Normal file
|
@ -0,0 +1,87 @@
|
|||
@import "../css/variables.module.scss";
|
||||
|
||||
.excalidraw {
|
||||
.ElementLinkDialog {
|
||||
position: absolute;
|
||||
top: var(--editor-container-padding);
|
||||
left: calc(var(--editor-container-padding) * 4);
|
||||
|
||||
z-index: 3;
|
||||
|
||||
border-radius: 10px;
|
||||
padding: 1.5rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
box-shadow: var(--shadow-island);
|
||||
background-color: var(--island-bg-color);
|
||||
|
||||
@include isMobile {
|
||||
left: 0;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
width: calc(100% - 1rem);
|
||||
box-sizing: border-box;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.ElementLinkDialog__header {
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
@include isMobile {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
|
||||
@include isMobile {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
@include isMobile {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ElementLinkDialog__input {
|
||||
display: flex;
|
||||
|
||||
.ElementLinkDialog__input-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ElementLinkDialog__remove {
|
||||
color: $oc-red-9;
|
||||
margin-left: 1rem;
|
||||
|
||||
.ToolIcon__icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.ToolIcon__icon svg {
|
||||
color: $oc-red-6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ElementLinkDialog__actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
@include isMobile {
|
||||
font-size: 0.875rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
174
packages/excalidraw/components/ElementLinkDialog.tsx
Normal file
174
packages/excalidraw/components/ElementLinkDialog.tsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
import { TextField } from "./TextField";
|
||||
import type { AppProps, AppState, UIAppState } from "../types";
|
||||
import DialogActionButton from "./DialogActionButton";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import {
|
||||
defaultGetElementLinkFromSelection,
|
||||
getLinkIdAndTypeFromSelection,
|
||||
} from "../element/elementLink";
|
||||
import { mutateElement } from "../element/mutateElement";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import type { ElementsMap, ExcalidrawElement } from "../element/types";
|
||||
import { ToolButton } from "./ToolButton";
|
||||
import { TrashIcon } from "./icons";
|
||||
import { KEYS } from "../keys";
|
||||
|
||||
import "./ElementLinkDialog.scss";
|
||||
import { normalizeLink } from "../data/url";
|
||||
|
||||
const ElementLinkDialog = ({
|
||||
sourceElementId,
|
||||
onClose,
|
||||
elementsMap,
|
||||
appState,
|
||||
generateLinkForSelection = defaultGetElementLinkFromSelection,
|
||||
}: {
|
||||
sourceElementId: ExcalidrawElement["id"];
|
||||
elementsMap: ElementsMap;
|
||||
appState: UIAppState;
|
||||
onClose?: () => void;
|
||||
generateLinkForSelection: AppProps["generateLinkForSelection"];
|
||||
}) => {
|
||||
const originalLink = elementsMap.get(sourceElementId)?.link ?? null;
|
||||
|
||||
const [nextLink, setNextLink] = useState<string | null>(originalLink);
|
||||
const [linkEdited, setLinkEdited] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const selectedElements = getSelectedElements(elementsMap, appState);
|
||||
let nextLink = originalLink;
|
||||
|
||||
if (selectedElements.length > 0 && generateLinkForSelection) {
|
||||
const idAndType = getLinkIdAndTypeFromSelection(
|
||||
selectedElements,
|
||||
appState as AppState,
|
||||
);
|
||||
|
||||
if (idAndType) {
|
||||
nextLink = normalizeLink(
|
||||
generateLinkForSelection(idAndType.id, idAndType.type),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
setNextLink(nextLink);
|
||||
}, [
|
||||
elementsMap,
|
||||
appState,
|
||||
appState.selectedElementIds,
|
||||
originalLink,
|
||||
generateLinkForSelection,
|
||||
]);
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
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]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
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 (
|
||||
<div className="ElementLinkDialog">
|
||||
<div className="ElementLinkDialog__header">
|
||||
<h2>{t("elementLink.title")}</h2>
|
||||
<p>{t("elementLink.desc")}</p>
|
||||
</div>
|
||||
|
||||
<div className="ElementLinkDialog__input">
|
||||
<TextField
|
||||
value={nextLink ?? ""}
|
||||
onChange={(value) => {
|
||||
if (!linkEdited) {
|
||||
setLinkEdited(true);
|
||||
}
|
||||
setNextLink(value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === KEYS.ENTER) {
|
||||
handleConfirm();
|
||||
}
|
||||
}}
|
||||
className="ElementLinkDialog__input-field"
|
||||
selectOnRender
|
||||
/>
|
||||
|
||||
{originalLink && nextLink && (
|
||||
<ToolButton
|
||||
type="button"
|
||||
title={t("buttons.remove")}
|
||||
aria-label={t("buttons.remove")}
|
||||
label={t("buttons.remove")}
|
||||
onClick={() => {
|
||||
// removes the link from the input
|
||||
// but doesn't update the element
|
||||
|
||||
// when confirmed, will remove the link from the element
|
||||
setNextLink(null);
|
||||
setLinkEdited(true);
|
||||
}}
|
||||
className="ElementLinkDialog__remove"
|
||||
icon={TrashIcon}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="ElementLinkDialog__actions">
|
||||
<DialogActionButton
|
||||
label={t("buttons.cancel")}
|
||||
onClick={() => {
|
||||
onClose?.();
|
||||
}}
|
||||
style={{
|
||||
marginRight: 10,
|
||||
}}
|
||||
/>
|
||||
|
||||
<DialogActionButton
|
||||
label={t("buttons.confirm")}
|
||||
onClick={handleConfirm}
|
||||
actionType="primary"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ElementLinkDialog;
|
|
@ -60,6 +60,7 @@ import { LaserPointerButton } from "./LaserPointerButton";
|
|||
import { TTDDialog } from "./TTDDialog/TTDDialog";
|
||||
import { Stats } from "./Stats";
|
||||
import { actionToggleStats } from "../actions";
|
||||
import ElementLinkDialog from "./ElementLinkDialog";
|
||||
|
||||
import "./LayerUI.scss";
|
||||
import "./Toolbar.scss";
|
||||
|
@ -84,6 +85,7 @@ interface LayerUIProps {
|
|||
children?: React.ReactNode;
|
||||
app: AppClassProperties;
|
||||
isCollaborating: boolean;
|
||||
generateLinkForSelection?: AppProps["generateLinkForSelection"];
|
||||
}
|
||||
|
||||
const DefaultMainMenu: React.FC<{
|
||||
|
@ -141,6 +143,7 @@ const LayerUI = ({
|
|||
children,
|
||||
app,
|
||||
isCollaborating,
|
||||
generateLinkForSelection,
|
||||
}: LayerUIProps) => {
|
||||
const device = useDevice();
|
||||
const tunnels = useInitializeTunnels();
|
||||
|
@ -232,7 +235,8 @@ const LayerUI = ({
|
|||
const shouldShowStats =
|
||||
appState.stats.open &&
|
||||
!appState.zenModeEnabled &&
|
||||
!appState.viewModeEnabled;
|
||||
!appState.viewModeEnabled &&
|
||||
appState.openDialog?.name !== "elementLinkSelector";
|
||||
|
||||
return (
|
||||
<FixedSideContainer side="top">
|
||||
|
@ -241,90 +245,91 @@ const LayerUI = ({
|
|||
{renderCanvasActions()}
|
||||
{shouldRenderSelectedShapeActions && renderSelectedShapeActions()}
|
||||
</Stack.Col>
|
||||
{!appState.viewModeEnabled && (
|
||||
<Section heading="shapes" className="shapes-section">
|
||||
{(heading: React.ReactNode) => (
|
||||
<div style={{ position: "relative" }}>
|
||||
{renderWelcomeScreen && (
|
||||
<tunnels.WelcomeScreenToolbarHintTunnel.Out />
|
||||
)}
|
||||
<Stack.Col gap={4} align="start">
|
||||
<Stack.Row
|
||||
gap={1}
|
||||
className={clsx("App-toolbar-container", {
|
||||
"zen-mode": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<Island
|
||||
padding={1}
|
||||
className={clsx("App-toolbar", {
|
||||
{!appState.viewModeEnabled &&
|
||||
appState.openDialog?.name !== "elementLinkSelector" && (
|
||||
<Section heading="shapes" className="shapes-section">
|
||||
{(heading: React.ReactNode) => (
|
||||
<div style={{ position: "relative" }}>
|
||||
{renderWelcomeScreen && (
|
||||
<tunnels.WelcomeScreenToolbarHintTunnel.Out />
|
||||
)}
|
||||
<Stack.Col gap={4} align="start">
|
||||
<Stack.Row
|
||||
gap={1}
|
||||
className={clsx("App-toolbar-container", {
|
||||
"zen-mode": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<HintViewer
|
||||
appState={appState}
|
||||
isMobile={device.editor.isMobile}
|
||||
device={device}
|
||||
app={app}
|
||||
/>
|
||||
{heading}
|
||||
<Stack.Row gap={1}>
|
||||
<PenModeButton
|
||||
zenModeEnabled={appState.zenModeEnabled}
|
||||
checked={appState.penMode}
|
||||
onChange={() => onPenModeToggle(null)}
|
||||
title={t("toolBar.penMode")}
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
<LockButton
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={onLockToggle}
|
||||
title={t("toolBar.lock")}
|
||||
/>
|
||||
|
||||
<div className="App-toolbar__divider" />
|
||||
|
||||
<HandButton
|
||||
checked={isHandToolActive(appState)}
|
||||
onChange={() => onHandToolToggle()}
|
||||
title={t("toolBar.hand")}
|
||||
isMobile
|
||||
/>
|
||||
|
||||
<ShapesSwitcher
|
||||
<Island
|
||||
padding={1}
|
||||
className={clsx("App-toolbar", {
|
||||
"zen-mode": appState.zenModeEnabled,
|
||||
})}
|
||||
>
|
||||
<HintViewer
|
||||
appState={appState}
|
||||
activeTool={appState.activeTool}
|
||||
UIOptions={UIOptions}
|
||||
isMobile={device.editor.isMobile}
|
||||
device={device}
|
||||
app={app}
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
{isCollaborating && (
|
||||
<Island
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
alignSelf: "center",
|
||||
height: "fit-content",
|
||||
}}
|
||||
>
|
||||
<LaserPointerButton
|
||||
title={t("toolBar.laser")}
|
||||
checked={
|
||||
appState.activeTool.type === TOOL_TYPE.laser
|
||||
}
|
||||
onChange={() =>
|
||||
app.setActiveTool({ type: TOOL_TYPE.laser })
|
||||
}
|
||||
isMobile
|
||||
/>
|
||||
{heading}
|
||||
<Stack.Row gap={1}>
|
||||
<PenModeButton
|
||||
zenModeEnabled={appState.zenModeEnabled}
|
||||
checked={appState.penMode}
|
||||
onChange={() => onPenModeToggle(null)}
|
||||
title={t("toolBar.penMode")}
|
||||
penDetected={appState.penDetected}
|
||||
/>
|
||||
<LockButton
|
||||
checked={appState.activeTool.locked}
|
||||
onChange={onLockToggle}
|
||||
title={t("toolBar.lock")}
|
||||
/>
|
||||
|
||||
<div className="App-toolbar__divider" />
|
||||
|
||||
<HandButton
|
||||
checked={isHandToolActive(appState)}
|
||||
onChange={() => onHandToolToggle()}
|
||||
title={t("toolBar.hand")}
|
||||
isMobile
|
||||
/>
|
||||
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
activeTool={appState.activeTool}
|
||||
UIOptions={UIOptions}
|
||||
app={app}
|
||||
/>
|
||||
</Stack.Row>
|
||||
</Island>
|
||||
)}
|
||||
</Stack.Row>
|
||||
</Stack.Col>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
{isCollaborating && (
|
||||
<Island
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
alignSelf: "center",
|
||||
height: "fit-content",
|
||||
}}
|
||||
>
|
||||
<LaserPointerButton
|
||||
title={t("toolBar.laser")}
|
||||
checked={
|
||||
appState.activeTool.type === TOOL_TYPE.laser
|
||||
}
|
||||
onChange={() =>
|
||||
app.setActiveTool({ type: TOOL_TYPE.laser })
|
||||
}
|
||||
isMobile
|
||||
/>
|
||||
</Island>
|
||||
)}
|
||||
</Stack.Row>
|
||||
</Stack.Col>
|
||||
</div>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
<div
|
||||
className={clsx(
|
||||
"layer-ui__wrapper__top-right zen-mode-transition",
|
||||
|
@ -341,6 +346,7 @@ const LayerUI = ({
|
|||
)}
|
||||
{renderTopRightUI?.(device.editor.isMobile, appState)}
|
||||
{!appState.viewModeEnabled &&
|
||||
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||
// hide button when sidebar docked
|
||||
(!isSidebarDocked ||
|
||||
appState.openSidebar?.name !== DEFAULT_SIDEBAR.name) && (
|
||||
|
@ -471,6 +477,19 @@ const LayerUI = ({
|
|||
/>
|
||||
)}
|
||||
<ActiveConfirmDialog />
|
||||
{appState.openDialog?.name === "elementLinkSelector" && (
|
||||
<ElementLinkDialog
|
||||
sourceElementId={appState.openDialog.sourceElementId}
|
||||
onClose={() => {
|
||||
setAppState({
|
||||
openDialog: null,
|
||||
});
|
||||
}}
|
||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||
appState={appState}
|
||||
generateLinkForSelection={generateLinkForSelection}
|
||||
/>
|
||||
)}
|
||||
<tunnels.OverwriteConfirmDialogTunnel.Out />
|
||||
{renderImageExportDialog()}
|
||||
{renderJSONExportDialog()}
|
||||
|
|
|
@ -91,9 +91,10 @@ export const MobileMenu = ({
|
|||
</Island>
|
||||
{renderTopRightUI && renderTopRightUI(true, appState)}
|
||||
<div className="mobile-misc-tools-container">
|
||||
{!appState.viewModeEnabled && (
|
||||
<DefaultSidebarTriggerTunnel.Out />
|
||||
)}
|
||||
{!appState.viewModeEnabled &&
|
||||
appState.openDialog?.name !== "elementLinkSelector" && (
|
||||
<DefaultSidebarTriggerTunnel.Out />
|
||||
)}
|
||||
<PenModeButton
|
||||
checked={appState.penMode}
|
||||
onChange={() => onPenModeToggle(null)}
|
||||
|
@ -129,7 +130,10 @@ export const MobileMenu = ({
|
|||
};
|
||||
|
||||
const renderAppToolbar = () => {
|
||||
if (appState.viewModeEnabled) {
|
||||
if (
|
||||
appState.viewModeEnabled ||
|
||||
appState.openDialog?.name === "elementLinkSelector"
|
||||
) {
|
||||
return (
|
||||
<div className="App-toolbar-content">
|
||||
<MainMenuTunnel.Out />
|
||||
|
@ -154,7 +158,9 @@ export const MobileMenu = ({
|
|||
return (
|
||||
<>
|
||||
{renderSidebars()}
|
||||
{!appState.viewModeEnabled && renderToolbar()}
|
||||
{!appState.viewModeEnabled &&
|
||||
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||
renderToolbar()}
|
||||
<div
|
||||
className="App-bottom-bar"
|
||||
style={{
|
||||
|
@ -166,6 +172,7 @@ export const MobileMenu = ({
|
|||
<Island padding={0}>
|
||||
{appState.openMenu === "shape" &&
|
||||
!appState.viewModeEnabled &&
|
||||
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||
showSelectedShapeActions(appState, elements) ? (
|
||||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||
<SelectedShapeActions
|
||||
|
|
|
@ -182,6 +182,7 @@ const getRelevantAppStateProps = (
|
|||
width: appState.width,
|
||||
height: appState.height,
|
||||
viewModeEnabled: appState.viewModeEnabled,
|
||||
openDialog: appState.openDialog,
|
||||
editingGroupId: appState.editingGroupId,
|
||||
editingLinearElement: appState.editingLinearElement,
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
|
|
|
@ -92,6 +92,8 @@ const getRelevantAppStateProps = (
|
|||
width: appState.width,
|
||||
height: appState.height,
|
||||
viewModeEnabled: appState.viewModeEnabled,
|
||||
openDialog: appState.openDialog,
|
||||
hoveredElementIds: appState.hoveredElementIds,
|
||||
offsetLeft: appState.offsetLeft,
|
||||
offsetTop: appState.offsetTop,
|
||||
theme: appState.theme,
|
||||
|
|
|
@ -13,7 +13,7 @@ import type {
|
|||
} from "../../element/types";
|
||||
|
||||
import { ToolButton } from "../ToolButton";
|
||||
import { FreedrawIcon, TrashIcon } from "../icons";
|
||||
import { FreedrawIcon, TrashIcon, elementLinkIcon } from "../icons";
|
||||
import { t } from "../../i18n";
|
||||
import {
|
||||
useCallback,
|
||||
|
@ -30,18 +30,19 @@ import { getTooltipDiv, updateTooltipPosition } from "../../components/Tooltip";
|
|||
import { getSelectedElements } from "../../scene";
|
||||
import { hitElementBoundingBox } from "../../element/collision";
|
||||
import { isLocalLink, normalizeLink } from "../../data/url";
|
||||
|
||||
import "./Hyperlink.scss";
|
||||
import { trackEvent } from "../../analytics";
|
||||
import { useAppProps, useExcalidrawAppState } from "../App";
|
||||
import { useAppProps, useDevice, useExcalidrawAppState } from "../App";
|
||||
import { isEmbeddableElement } from "../../element/typeChecks";
|
||||
import { getLinkHandleFromCoords } from "./helpers";
|
||||
import { pointFrom, type GlobalPoint } from "../../../math";
|
||||
import { isElementLink } from "../../element/elementLink";
|
||||
|
||||
const CONTAINER_WIDTH = 320;
|
||||
import "./Hyperlink.scss";
|
||||
|
||||
const POPUP_WIDTH = 380;
|
||||
const POPUP_HEIGHT = 42;
|
||||
const POPUP_PADDING = 5;
|
||||
const SPACE_BOTTOM = 85;
|
||||
const CONTAINER_PADDING = 5;
|
||||
const CONTAINER_HEIGHT = 42;
|
||||
const AUTO_HIDE_TIMEOUT = 500;
|
||||
|
||||
let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
|
||||
|
@ -73,6 +74,7 @@ export const Hyperlink = ({
|
|||
}) => {
|
||||
const appState = useExcalidrawAppState();
|
||||
const appProps = useAppProps();
|
||||
const device = useDevice();
|
||||
|
||||
const linkVal = element.link || "";
|
||||
|
||||
|
@ -170,6 +172,15 @@ export const Hyperlink = ({
|
|||
|
||||
useEffect(() => {
|
||||
let timeoutId: number | null = null;
|
||||
|
||||
if (
|
||||
inputRef &&
|
||||
inputRef.current &&
|
||||
!(device.viewport.isMobile || device.isTouchScreen)
|
||||
) {
|
||||
inputRef.current.select();
|
||||
}
|
||||
|
||||
const handlePointerMove = (event: PointerEvent) => {
|
||||
if (isEditing) {
|
||||
return;
|
||||
|
@ -196,16 +207,21 @@ export const Hyperlink = ({
|
|||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [appState, element, isEditing, setAppState, elementsMap]);
|
||||
}, [
|
||||
appState,
|
||||
element,
|
||||
isEditing,
|
||||
setAppState,
|
||||
elementsMap,
|
||||
device.viewport.isMobile,
|
||||
device.isTouchScreen,
|
||||
]);
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
trackEvent("hyperlink", "delete");
|
||||
mutateElement(element, { link: null });
|
||||
if (isEditing) {
|
||||
inputRef.current!.value = "";
|
||||
}
|
||||
setAppState({ showHyperlinkPopup: false });
|
||||
}, [setAppState, element, isEditing]);
|
||||
}, [setAppState, element]);
|
||||
|
||||
const onEdit = () => {
|
||||
trackEvent("hyperlink", "edit", "popup-ui");
|
||||
|
@ -229,19 +245,14 @@ export const Hyperlink = ({
|
|||
style={{
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
width: CONTAINER_WIDTH,
|
||||
padding: CONTAINER_PADDING,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!element.link && !isEditing) {
|
||||
setAppState({ showHyperlinkPopup: "editor" });
|
||||
}
|
||||
width: POPUP_WIDTH,
|
||||
padding: POPUP_PADDING,
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<input
|
||||
className={clsx("excalidraw-hyperlinkContainer-input")}
|
||||
placeholder="Type or paste your link here"
|
||||
placeholder={t("labels.link.hint")}
|
||||
ref={inputRef}
|
||||
value={inputVal}
|
||||
onChange={(event) => setInputVal(event.target.value)}
|
||||
|
@ -302,6 +313,21 @@ export const Hyperlink = ({
|
|||
icon={FreedrawIcon}
|
||||
/>
|
||||
)}
|
||||
<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) && (
|
||||
<ToolButton
|
||||
type="button"
|
||||
|
@ -328,7 +354,7 @@ const getCoordsForPopover = (
|
|||
{ sceneX: x1 + element.width / 2, sceneY: y1 },
|
||||
appState,
|
||||
);
|
||||
const x = viewportX - appState.offsetLeft - CONTAINER_WIDTH / 2;
|
||||
const x = viewportX - appState.offsetLeft - POPUP_WIDTH / 2;
|
||||
const y = viewportY - appState.offsetTop - SPACE_BOTTOM;
|
||||
return { x, y };
|
||||
};
|
||||
|
@ -338,12 +364,10 @@ export const getContextMenuLabel = (
|
|||
appState: UIAppState,
|
||||
) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
const label = selectedElements[0]?.link
|
||||
? isEmbeddableElement(selectedElements[0])
|
||||
? "labels.link.editEmbed"
|
||||
: "labels.link.edit"
|
||||
: isEmbeddableElement(selectedElements[0])
|
||||
? "labels.link.createEmbed"
|
||||
const label = isEmbeddableElement(selectedElements[0])
|
||||
? "labels.link.editEmbed"
|
||||
: selectedElements[0]?.link
|
||||
? "labels.link.edit"
|
||||
: "labels.link.create";
|
||||
return label;
|
||||
};
|
||||
|
@ -376,7 +400,9 @@ const renderTooltip = (
|
|||
|
||||
tooltipDiv.classList.add("excalidraw-tooltip--visible");
|
||||
tooltipDiv.style.maxWidth = "20rem";
|
||||
tooltipDiv.textContent = element.link;
|
||||
tooltipDiv.textContent = isElementLink(element.link)
|
||||
? t("labels.link.goToElement")
|
||||
: element.link;
|
||||
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
|
||||
|
||||
|
@ -450,9 +476,9 @@ const shouldHideLinkPopup = (
|
|||
|
||||
if (
|
||||
clientX >= popoverX - threshold &&
|
||||
clientX <= popoverX + CONTAINER_WIDTH + CONTAINER_PADDING * 2 + threshold &&
|
||||
clientX <= popoverX + POPUP_WIDTH + POPUP_PADDING * 2 + threshold &&
|
||||
clientY >= popoverY - threshold &&
|
||||
clientY <= popoverY + threshold + CONTAINER_PADDING * 2 + CONTAINER_HEIGHT
|
||||
clientY <= popoverY + threshold + POPUP_PADDING * 2 + POPUP_HEIGHT
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -16,6 +16,11 @@ EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
|||
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
|
||||
)}`;
|
||||
|
||||
export const ELEMENT_LINK_IMG = document.createElement("img");
|
||||
ELEMENT_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-big-right-line"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 9v-3.586a1 1 0 0 1 1.707 -.707l6.586 6.586a1 1 0 0 1 0 1.414l-6.586 6.586a1 1 0 0 1 -1.707 -.707v-3.586h-6v-6h6z" /><path d="M3 9v6" /></svg>`,
|
||||
)}`;
|
||||
|
||||
export const getLinkHandleFromCoords = (
|
||||
[x1, y1, x2, y2]: Bounds,
|
||||
angle: Radians,
|
||||
|
|
|
@ -2156,3 +2156,18 @@ export const cropIcon = createIcon(
|
|||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
||||
export const elementLinkIcon = createIcon(
|
||||
<g>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M5 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M19 5m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M5 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M19 19m-2 0a2 2 0 1 0 4 0a2 2 0 1 0 -4 0" />
|
||||
<path d="M5 7l0 10" />
|
||||
<path d="M7 5l10 0" />
|
||||
<path d="M7 19l10 0" />
|
||||
<path d="M19 7l0 10" />
|
||||
</g>,
|
||||
tablerIconProps,
|
||||
);
|
||||
|
|
|
@ -449,3 +449,6 @@ export const ARROW_TYPE: { [T in AppState["currentItemArrowType"]]: T } = {
|
|||
round: "round",
|
||||
elbow: "elbow",
|
||||
};
|
||||
|
||||
export const DEFAULT_REDUCED_GLOBAL_ALPHA = 0.3;
|
||||
export const ELEMENT_LINK_KEY = "element";
|
||||
|
|
102
packages/excalidraw/element/elementLink.ts
Normal file
102
packages/excalidraw/element/elementLink.ts
Normal file
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* Create and link between shapes.
|
||||
*/
|
||||
|
||||
import { ELEMENT_LINK_KEY } from "../constants";
|
||||
import { normalizeLink } from "../data/url";
|
||||
import { elementsAreInSameGroup } from "../groups";
|
||||
import type { AppProps, AppState } from "../types";
|
||||
import type { ExcalidrawElement } from "./types";
|
||||
|
||||
export const defaultGetElementLinkFromSelection: Exclude<
|
||||
AppProps["generateLinkForSelection"],
|
||||
undefined
|
||||
> = (id, type) => {
|
||||
const url = window.location.href;
|
||||
|
||||
try {
|
||||
const link = new URL(url);
|
||||
link.searchParams.set(ELEMENT_LINK_KEY, id);
|
||||
|
||||
return normalizeLink(link.toString());
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return normalizeLink(url);
|
||||
};
|
||||
|
||||
export const getLinkIdAndTypeFromSelection = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
): {
|
||||
id: string;
|
||||
type: "element" | "group";
|
||||
} | null => {
|
||||
if (
|
||||
selectedElements.length > 0 &&
|
||||
canCreateLinkFromElements(selectedElements)
|
||||
) {
|
||||
if (selectedElements.length === 1) {
|
||||
return {
|
||||
id: selectedElements[0].id,
|
||||
type: "element",
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedElements.length > 1) {
|
||||
const selectedGroupId = Object.keys(appState.selectedGroupIds)[0];
|
||||
|
||||
if (selectedGroupId) {
|
||||
return {
|
||||
id: selectedGroupId,
|
||||
type: "group",
|
||||
};
|
||||
}
|
||||
return {
|
||||
id: selectedElements[0].groupIds[0],
|
||||
type: "group",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const canCreateLinkFromElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
) => {
|
||||
if (selectedElements.length === 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selectedElements.length > 1 && elementsAreInSameGroup(selectedElements)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const isElementLink = (url: string) => {
|
||||
try {
|
||||
const _url = new URL(url);
|
||||
return (
|
||||
_url.searchParams.has(ELEMENT_LINK_KEY) &&
|
||||
_url.host === window.location.host
|
||||
);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const parseElementLinkFromURL = (url: string) => {
|
||||
try {
|
||||
const { searchParams } = new URL(url);
|
||||
if (searchParams.has(ELEMENT_LINK_KEY)) {
|
||||
const id = searchParams.get(ELEMENT_LINK_KEY);
|
||||
return id;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -8,6 +8,7 @@ export const showSelectedShapeActions = (
|
|||
) =>
|
||||
Boolean(
|
||||
!appState.viewModeEnabled &&
|
||||
appState.openDialog?.name !== "elementLinkSelector" &&
|
||||
((appState.activeTool.type !== "custom" &&
|
||||
(appState.editingTextElement ||
|
||||
(appState.activeTool.type !== "selection" &&
|
||||
|
|
|
@ -43,6 +43,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
autoFocus = false,
|
||||
generateIdForFile,
|
||||
onLinkOpen,
|
||||
generateLinkForSelection,
|
||||
onPointerDown,
|
||||
onPointerUp,
|
||||
onScrollChange,
|
||||
|
@ -132,6 +133,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
autoFocus={autoFocus}
|
||||
generateIdForFile={generateIdForFile}
|
||||
onLinkOpen={onLinkOpen}
|
||||
generateLinkForSelection={generateLinkForSelection}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
onScrollChange={onScrollChange}
|
||||
|
@ -291,3 +293,4 @@ export {
|
|||
|
||||
export { DiagramToCodePlugin } from "./components/DiagramToCodePlugin/DiagramToCodePlugin";
|
||||
export { getDataURL } from "./data/blob";
|
||||
export { isElementLink } from "./element/elementLink";
|
||||
|
|
|
@ -125,12 +125,13 @@
|
|||
"createContainerFromText": "Wrap text in a container",
|
||||
"link": {
|
||||
"edit": "Edit link",
|
||||
"editEmbed": "Edit link & embed",
|
||||
"create": "Create link",
|
||||
"createEmbed": "Create link & embed",
|
||||
"editEmbed": "Edit embeddable link",
|
||||
"create": "Add link",
|
||||
"label": "Link",
|
||||
"labelEmbed": "Link & embed",
|
||||
"empty": "No link is set"
|
||||
"empty": "No link is set",
|
||||
"hint": "Type or paste your link here",
|
||||
"goToElement": "Go to target element"
|
||||
},
|
||||
"lineEditor": {
|
||||
"edit": "Edit line",
|
||||
|
@ -155,7 +156,14 @@
|
|||
"zoomToFitSelection": "Zoom to fit selection",
|
||||
"zoomToFit": "Zoom to fit all elements",
|
||||
"installPWA": "Install Excalidraw locally (PWA)",
|
||||
"autoResize": "Enable text auto-resizing"
|
||||
"autoResize": "Enable text auto-resizing",
|
||||
"copyElementLink": "Copy link to object",
|
||||
"linkToElement": "Link to object"
|
||||
},
|
||||
"elementLink": {
|
||||
"title": "Link to object",
|
||||
"desc": "Click on a shape on canvas or paste a link.",
|
||||
"notFound": "Linked object wasn't found on canvas."
|
||||
},
|
||||
"library": {
|
||||
"noItems": "No items added yet...",
|
||||
|
@ -501,7 +509,8 @@
|
|||
"selection": "selection",
|
||||
"pasteAsSingleElement": "Use {{shortcut}} to paste as a single element,\nor paste into an existing text editor",
|
||||
"unableToEmbed": "Embedding this url is currently not allowed. Raise an issue on GitHub to request the url whitelisted",
|
||||
"unrecognizedLinkFormat": "The link you embedded does not match the expected format. Please try to paste the 'embed' string provided by the source site"
|
||||
"unrecognizedLinkFormat": "The link you embedded does not match the expected format. Please try to paste the 'embed' string provided by the source site",
|
||||
"elementLinkCopied": "Link copied to clipboard"
|
||||
},
|
||||
"colors": {
|
||||
"transparent": "Transparent",
|
||||
|
|
|
@ -40,6 +40,7 @@ import type {
|
|||
import { getDefaultAppState } from "../appState";
|
||||
import {
|
||||
BOUND_TEXT_PADDING,
|
||||
DEFAULT_REDUCED_GLOBAL_ALPHA,
|
||||
ELEMENT_READY_TO_ERASE_OPACITY,
|
||||
FRAME_STYLE,
|
||||
MIME_TYPES,
|
||||
|
@ -109,10 +110,13 @@ export const getRenderOpacity = (
|
|||
containingFrame: ExcalidrawFrameLikeElement | null,
|
||||
elementsPendingErasure: ElementsPendingErasure,
|
||||
pendingNodes: Readonly<PendingExcalidrawElements> | null,
|
||||
globalAlpha: number = 1,
|
||||
) => {
|
||||
// multiplying frame opacity with element opacity to combine them
|
||||
// (e.g. frame 50% and element 50% opacity should result in 25% opacity)
|
||||
let opacity = ((containingFrame?.opacity ?? 100) * element.opacity) / 10000;
|
||||
let opacity =
|
||||
(((containingFrame?.opacity ?? 100) * element.opacity) / 10000) *
|
||||
globalAlpha;
|
||||
|
||||
// if pending erasure, multiply again to combine further
|
||||
// (so that erasing always results in lower opacity than original)
|
||||
|
@ -700,11 +704,17 @@ export const renderElement = (
|
|||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
const reduceAlphaForSelection =
|
||||
appState.openDialog?.name === "elementLinkSelector" &&
|
||||
!appState.selectedElementIds[element.id] &&
|
||||
!appState.hoveredElementIds[element.id];
|
||||
|
||||
context.globalAlpha = getRenderOpacity(
|
||||
element,
|
||||
getContainingFrame(element, elementsMap),
|
||||
renderConfig.elementsPendingErasure,
|
||||
renderConfig.pendingFlowchartNodes,
|
||||
reduceAlphaForSelection ? DEFAULT_REDUCED_GLOBAL_ALPHA : 1,
|
||||
);
|
||||
|
||||
switch (element.type) {
|
||||
|
|
|
@ -25,11 +25,13 @@ import type {
|
|||
} from "../scene/types";
|
||||
import {
|
||||
EXTERNAL_LINK_IMG,
|
||||
ELEMENT_LINK_IMG,
|
||||
getLinkHandleFromCoords,
|
||||
} from "../components/hyperlink/helpers";
|
||||
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
|
||||
import { throttleRAF } from "../utils";
|
||||
import { getBoundTextElement } from "../element/textElement";
|
||||
import { isElementLink } from "../element/elementLink";
|
||||
|
||||
const GridLineColor = {
|
||||
Bold: "#dddddd",
|
||||
|
@ -133,7 +135,16 @@ const frameClip = (
|
|||
);
|
||||
};
|
||||
|
||||
let linkCanvasCache: any;
|
||||
type LinkIconCanvas = HTMLCanvasElement & { zoom: number };
|
||||
|
||||
const linkIconCanvasCache: {
|
||||
regularLink: LinkIconCanvas | null;
|
||||
elementLink: LinkIconCanvas | null;
|
||||
} = {
|
||||
regularLink: null,
|
||||
elementLink: null,
|
||||
};
|
||||
|
||||
const renderLinkIcon = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
context: CanvasRenderingContext2D,
|
||||
|
@ -153,38 +164,44 @@ const renderLinkIcon = (
|
|||
context.translate(appState.scrollX + centerX, appState.scrollY + centerY);
|
||||
context.rotate(element.angle);
|
||||
|
||||
if (!linkCanvasCache || linkCanvasCache.zoom !== appState.zoom.value) {
|
||||
linkCanvasCache = document.createElement("canvas");
|
||||
linkCanvasCache.zoom = appState.zoom.value;
|
||||
linkCanvasCache.width =
|
||||
width * window.devicePixelRatio * appState.zoom.value;
|
||||
linkCanvasCache.height =
|
||||
const canvasKey = isElementLink(element.link)
|
||||
? "elementLink"
|
||||
: "regularLink";
|
||||
|
||||
let linkCanvas = linkIconCanvasCache[canvasKey];
|
||||
|
||||
if (!linkCanvas || linkCanvas.zoom !== appState.zoom.value) {
|
||||
linkCanvas = Object.assign(document.createElement("canvas"), {
|
||||
zoom: appState.zoom.value,
|
||||
});
|
||||
linkCanvas.width = width * window.devicePixelRatio * appState.zoom.value;
|
||||
linkCanvas.height =
|
||||
height * window.devicePixelRatio * appState.zoom.value;
|
||||
const linkCanvasCacheContext = linkCanvasCache.getContext("2d")!;
|
||||
linkIconCanvasCache[canvasKey] = linkCanvas;
|
||||
|
||||
const linkCanvasCacheContext = linkCanvas.getContext("2d")!;
|
||||
linkCanvasCacheContext.scale(
|
||||
window.devicePixelRatio * appState.zoom.value,
|
||||
window.devicePixelRatio * appState.zoom.value,
|
||||
);
|
||||
linkCanvasCacheContext.fillStyle = "#fff";
|
||||
linkCanvasCacheContext.fillRect(0, 0, width, height);
|
||||
linkCanvasCacheContext.drawImage(EXTERNAL_LINK_IMG, 0, 0, width, height);
|
||||
|
||||
if (canvasKey === "elementLink") {
|
||||
linkCanvasCacheContext.drawImage(ELEMENT_LINK_IMG, 0, 0, width, height);
|
||||
} else {
|
||||
linkCanvasCacheContext.drawImage(
|
||||
EXTERNAL_LINK_IMG,
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
|
||||
linkCanvasCacheContext.restore();
|
||||
context.drawImage(
|
||||
linkCanvasCache,
|
||||
x - centerX,
|
||||
y - centerY,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
} else {
|
||||
context.drawImage(
|
||||
linkCanvasCache,
|
||||
x - centerX,
|
||||
y - centerY,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
context.drawImage(linkCanvas, x - centerX, y - centerY, width, height);
|
||||
context.restore();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -25,6 +25,7 @@ import {
|
|||
import { arrayToMap } from "../utils";
|
||||
import { toBrandedType } from "../utils";
|
||||
import { ENV } from "../constants";
|
||||
import { getElementsInGroup } from "../groups";
|
||||
|
||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
|
@ -437,6 +438,18 @@ class Scene {
|
|||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
getElementsFromId = (id: string): ExcalidrawElement[] => {
|
||||
const elementsMap = this.getNonDeletedElementsMap();
|
||||
// first check if the id is an element
|
||||
const el = elementsMap.get(id);
|
||||
if (el) {
|
||||
return [el];
|
||||
}
|
||||
|
||||
// then, check if the id is a group
|
||||
return getElementsInGroup(elementsMap, id);
|
||||
};
|
||||
}
|
||||
|
||||
export default Scene;
|
||||
|
|
|
@ -728,6 +728,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||
"category": "element",
|
||||
},
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
"PanelComponent": [Function],
|
||||
"icon": <svg
|
||||
|
@ -762,6 +763,42 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||
"category": "hyperlink",
|
||||
},
|
||||
},
|
||||
{
|
||||
"icon": <svg
|
||||
aria-hidden="true"
|
||||
className=""
|
||||
fill="none"
|
||||
focusable="false"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<React.Fragment>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
||||
/>
|
||||
<path
|
||||
d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"
|
||||
/>
|
||||
</React.Fragment>
|
||||
</svg>,
|
||||
"label": "labels.copyElementLink",
|
||||
"name": "copyElementLink",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
"PanelComponent": [Function],
|
||||
"icon": <svg
|
||||
|
@ -880,6 +917,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -1088,6 +1126,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -1306,6 +1345,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -1639,6 +1679,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -1972,6 +2013,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -2190,6 +2232,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -2432,6 +2475,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -2735,6 +2779,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -3106,6 +3151,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -3583,6 +3629,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -3908,6 +3955,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -4233,6 +4281,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -5313,6 +5362,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||
"category": "element",
|
||||
},
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
"PanelComponent": [Function],
|
||||
"icon": <svg
|
||||
|
@ -5347,6 +5397,42 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||
"category": "hyperlink",
|
||||
},
|
||||
},
|
||||
{
|
||||
"icon": <svg
|
||||
aria-hidden="true"
|
||||
className=""
|
||||
fill="none"
|
||||
focusable="false"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<React.Fragment>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
||||
/>
|
||||
<path
|
||||
d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"
|
||||
/>
|
||||
</React.Fragment>
|
||||
</svg>,
|
||||
"label": "labels.copyElementLink",
|
||||
"name": "copyElementLink",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
"PanelComponent": [Function],
|
||||
"icon": <svg
|
||||
|
@ -5465,6 +5551,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -6486,6 +6573,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||
"category": "element",
|
||||
},
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
"PanelComponent": [Function],
|
||||
"icon": <svg
|
||||
|
@ -6520,6 +6608,42 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||
"category": "hyperlink",
|
||||
},
|
||||
},
|
||||
{
|
||||
"icon": <svg
|
||||
aria-hidden="true"
|
||||
className=""
|
||||
fill="none"
|
||||
focusable="false"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<React.Fragment>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
||||
/>
|
||||
<path
|
||||
d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"
|
||||
/>
|
||||
</React.Fragment>
|
||||
</svg>,
|
||||
"label": "labels.copyElementLink",
|
||||
"name": "copyElementLink",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
"PanelComponent": [Function],
|
||||
"icon": <svg
|
||||
|
@ -6638,6 +6762,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -7575,6 +7700,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -8381,6 +8507,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||
"category": "element",
|
||||
},
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
"PanelComponent": [Function],
|
||||
"icon": <svg
|
||||
|
@ -8415,6 +8542,42 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||
"category": "hyperlink",
|
||||
},
|
||||
},
|
||||
{
|
||||
"icon": <svg
|
||||
aria-hidden="true"
|
||||
className=""
|
||||
fill="none"
|
||||
focusable="false"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<React.Fragment>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
||||
/>
|
||||
<path
|
||||
d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"
|
||||
/>
|
||||
</React.Fragment>
|
||||
</svg>,
|
||||
"label": "labels.copyElementLink",
|
||||
"name": "copyElementLink",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
"PanelComponent": [Function],
|
||||
"icon": <svg
|
||||
|
@ -8533,6 +8696,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -9321,6 +9485,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||
"category": "element",
|
||||
},
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
"PanelComponent": [Function],
|
||||
"icon": <svg
|
||||
|
@ -9355,6 +9520,42 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||
"category": "hyperlink",
|
||||
},
|
||||
},
|
||||
{
|
||||
"icon": <svg
|
||||
aria-hidden="true"
|
||||
className=""
|
||||
fill="none"
|
||||
focusable="false"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<React.Fragment>
|
||||
<path
|
||||
d="M0 0h24v24H0z"
|
||||
fill="none"
|
||||
stroke="none"
|
||||
/>
|
||||
<path
|
||||
d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"
|
||||
/>
|
||||
<path
|
||||
d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"
|
||||
/>
|
||||
</React.Fragment>
|
||||
</svg>,
|
||||
"label": "labels.copyElementLink",
|
||||
"name": "copyElementLink",
|
||||
"perform": [Function],
|
||||
"predicate": [Function],
|
||||
"trackEvent": {
|
||||
"category": "element",
|
||||
},
|
||||
},
|
||||
"separator",
|
||||
{
|
||||
"PanelComponent": [Function],
|
||||
"icon": <svg
|
||||
|
@ -9473,6 +9674,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 100,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
|
|
@ -53,6 +53,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -657,6 +658,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -1165,6 +1167,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -1535,6 +1538,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -1906,6 +1910,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -2175,6 +2180,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -2617,6 +2623,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -2918,6 +2925,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -3204,6 +3212,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -3500,6 +3509,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -3788,6 +3798,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -4025,6 +4036,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -4286,6 +4298,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -4561,6 +4574,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -4794,6 +4808,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -5027,6 +5042,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -5258,6 +5274,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -5489,6 +5506,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -5750,6 +5768,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -6083,6 +6102,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -6510,6 +6530,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -6890,6 +6911,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -7211,6 +7233,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -7511,6 +7534,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -7742,6 +7766,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -8099,6 +8124,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -8456,6 +8482,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -8862,6 +8889,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -9151,6 +9179,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -9418,6 +9447,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -9684,6 +9714,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -9917,6 +9948,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -10220,6 +10252,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -10562,6 +10595,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -10799,6 +10833,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -11254,6 +11289,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -11510,6 +11546,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -11751,6 +11788,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -11994,6 +12032,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -12397,6 +12436,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -12646,6 +12686,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -12889,6 +12930,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -13132,6 +13174,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -13381,6 +13424,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -13715,6 +13759,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -13889,6 +13934,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -14179,6 +14225,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -14448,6 +14495,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -14725,6 +14773,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -14888,6 +14937,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -15586,6 +15636,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -16208,6 +16259,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -16830,6 +16882,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -17544,6 +17597,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -18296,6 +18350,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -18772,6 +18827,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -19296,6 +19352,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -19754,6 +19811,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 0,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
|
|
@ -53,6 +53,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -467,6 +468,7 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -872,6 +874,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": false,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -1416,6 +1419,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -1619,6 +1623,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -1993,6 +1998,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -2232,6 +2238,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -2411,6 +2418,7 @@ exports[`regression tests > can drag element that covers another element, while
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -2730,6 +2738,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -2975,6 +2984,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -3217,6 +3227,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -3446,6 +3457,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -3701,6 +3713,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -4011,6 +4024,7 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -4424,6 +4438,7 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -4706,6 +4721,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -4958,6 +4974,7 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -5167,6 +5184,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -5365,6 +5383,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -5746,6 +5765,7 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -6035,6 +6055,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -6842,6 +6863,7 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -7171,6 +7193,7 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -7446,6 +7469,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -7679,6 +7703,7 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -7915,6 +7940,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -8094,6 +8120,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -8273,6 +8300,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -8452,6 +8480,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -8674,6 +8703,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -8895,6 +8925,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -9088,6 +9119,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -9310,6 +9342,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -9489,6 +9522,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -9710,6 +9744,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -9889,6 +9924,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -10082,6 +10118,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -10261,6 +10298,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -10774,6 +10812,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -11050,6 +11089,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -11175,6 +11215,7 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -11373,6 +11414,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -11683,6 +11725,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -12094,6 +12137,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -12706,6 +12750,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -12834,6 +12879,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -13417,6 +13463,7 @@ exports[`regression tests > switches from group of selected elements to another
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -13754,6 +13801,7 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -14018,6 +14066,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -14143,6 +14192,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -14521,6 +14571,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
@ -14646,6 +14697,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"height": 768,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
|
|
@ -22,6 +22,7 @@ import { copiedStyles } from "../actions/actionStyles";
|
|||
import { API } from "./helpers/api";
|
||||
import { setDateTimeForTests } from "../utils";
|
||||
import { vi } from "vitest";
|
||||
import type { ActionName } from "../actions/types";
|
||||
|
||||
const checkpoint = (name: string) => {
|
||||
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
||||
|
@ -115,7 +116,7 @@ describe("contextMenu element", () => {
|
|||
const contextMenu = UI.queryContextMenu();
|
||||
const contextMenuOptions =
|
||||
contextMenu?.querySelectorAll(".context-menu li");
|
||||
const expectedShortcutNames: ShortcutName[] = [
|
||||
const expectedContextMenuItems: ActionName[] = [
|
||||
"cut",
|
||||
"copy",
|
||||
"paste",
|
||||
|
@ -131,14 +132,15 @@ describe("contextMenu element", () => {
|
|||
"bringToFront",
|
||||
"duplicateSelection",
|
||||
"hyperlink",
|
||||
"copyElementLink",
|
||||
"toggleElementLock",
|
||||
];
|
||||
|
||||
expect(contextMenu).not.toBeNull();
|
||||
expect(contextMenuOptions?.length).toBe(expectedShortcutNames.length);
|
||||
expectedShortcutNames.forEach((shortcutName) => {
|
||||
expect(contextMenuOptions?.length).toBe(expectedContextMenuItems.length);
|
||||
expectedContextMenuItems.forEach((item) => {
|
||||
expect(
|
||||
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
||||
contextMenu?.querySelector(`li[data-testid="${item}"]`),
|
||||
).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
@ -263,13 +265,14 @@ describe("contextMenu element", () => {
|
|||
const contextMenu = UI.queryContextMenu();
|
||||
const contextMenuOptions =
|
||||
contextMenu?.querySelectorAll(".context-menu li");
|
||||
const expectedShortcutNames: ShortcutName[] = [
|
||||
const expectedContextMenuItems: ActionName[] = [
|
||||
"cut",
|
||||
"copy",
|
||||
"paste",
|
||||
"copyStyles",
|
||||
"pasteStyles",
|
||||
"deleteSelectedElements",
|
||||
"copyElementLink",
|
||||
"ungroup",
|
||||
"addToLibrary",
|
||||
"flipHorizontal",
|
||||
|
@ -283,10 +286,10 @@ describe("contextMenu element", () => {
|
|||
];
|
||||
|
||||
expect(contextMenu).not.toBeNull();
|
||||
expect(contextMenuOptions?.length).toBe(expectedShortcutNames.length);
|
||||
expectedShortcutNames.forEach((shortcutName) => {
|
||||
expect(contextMenuOptions?.length).toBe(expectedContextMenuItems.length);
|
||||
expectedContextMenuItems.forEach((item) => {
|
||||
expect(
|
||||
contextMenu?.querySelector(`li[data-testid="${shortcutName}"]`),
|
||||
contextMenu?.querySelector(`li[data-testid="${item}"]`),
|
||||
).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -161,6 +161,7 @@ type _CommonCanvasAppState = {
|
|||
width: AppState["width"];
|
||||
height: AppState["height"];
|
||||
viewModeEnabled: AppState["viewModeEnabled"];
|
||||
openDialog: AppState["openDialog"];
|
||||
editingGroupId: AppState["editingGroupId"]; // TODO: move to interactive canvas if possible
|
||||
selectedElementIds: AppState["selectedElementIds"]; // TODO: move to interactive canvas if possible
|
||||
frameToHighlight: AppState["frameToHighlight"]; // TODO: move to interactive canvas if possible
|
||||
|
@ -181,6 +182,7 @@ export type StaticCanvasAppState = Readonly<
|
|||
gridStep: AppState["gridStep"];
|
||||
frameRendering: AppState["frameRendering"];
|
||||
currentHoveredFontFamily: AppState["currentHoveredFontFamily"];
|
||||
hoveredElementIds: AppState["hoveredElementIds"];
|
||||
// Cropping
|
||||
croppingElementId: AppState["croppingElementId"];
|
||||
}
|
||||
|
@ -332,7 +334,9 @@ export interface AppState {
|
|||
| null
|
||||
| { name: "imageExport" | "help" | "jsonExport" }
|
||||
| { name: "ttd"; tab: "text-to-diagram" | "mermaid" }
|
||||
| { name: "commandPalette" };
|
||||
| { name: "commandPalette" }
|
||||
| { name: "elementLinkSelector"; sourceElementId: ExcalidrawElement["id"] };
|
||||
|
||||
/**
|
||||
* Reflects user preference for whether the default sidebar should be docked.
|
||||
*
|
||||
|
@ -344,6 +348,7 @@ export interface AppState {
|
|||
|
||||
lastPointerDownWith: PointerType;
|
||||
selectedElementIds: Readonly<{ [id: string]: true }>;
|
||||
hoveredElementIds: Readonly<{ [id: string]: true }>;
|
||||
previousSelectedElementIds: { [id: string]: true };
|
||||
selectedElementsAreBeingDragged: boolean;
|
||||
shouldCacheIgnoreZoom: boolean;
|
||||
|
@ -530,6 +535,7 @@ export interface ExcalidrawProps {
|
|||
onLibraryChange?: (libraryItems: LibraryItems) => void | Promise<any>;
|
||||
autoFocus?: boolean;
|
||||
generateIdForFile?: (file: File) => string | Promise<string>;
|
||||
generateLinkForSelection?: (id: string, type: "element" | "group") => string;
|
||||
onLinkOpen?: (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
event: CustomEvent<{
|
||||
|
|
|
@ -53,6 +53,7 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||
"gridModeEnabled": false,
|
||||
"gridSize": 20,
|
||||
"gridStep": 5,
|
||||
"hoveredElementIds": {},
|
||||
"isBindingEnabled": true,
|
||||
"isCropping": false,
|
||||
"isLoading": false,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue