feat: Support hyperlinks 🔥 (#4620)
* feat: Support hypelinks * dont show edit when link not present * auto submit on blur * Add link button in sidebar and do it react way * add key to hyperlink to remount when element selection changes * autofocus input * remove click handler and use pointerup/down to show /hide popup * add keydown and support enter/escape to submit * show extrrnal link icon when element has link * use icons and open link in new tab * dnt submit unless link updated * renamed ffiles * remove unnecessary changes * update snap * hide link popup once user starts interacting with element and show again only if clicked outside and clicked on element again * render link icon outside the element * fix hit testing * rewrite implementation to render hyperlinks outside elements and hide when element selected * remove * remove * tweak icon position and size * rotate link icon when element rotated, handle zooming and render exactly where ne resize handle is rendered * no need to create a new reference anymore for element when link added/updated * rotate the link image as well when rotating element * calculate hitbox of link icon and show pointer when hovering over link icon * open link when clicked on link icon * show tooltip when hovering over link icon * show link action only when single element selected * support other protocols * add shortcut cmd/ctrl+k to edit/update link * don't hide popup after submit * renderes decreased woo * Add context mneu label to add/edit link * fix tests * remove tick and show trash when in edit mode * show edit view when element contains link * fix snap * horizontally center the hyperlink container with respect to elemnt * fix padding * remove checkcircle * show popup on hover of selected element and dismiss when outside hitbox * check if element has link before setting popup state * move logic of auto hide to hyperlink and dnt hide when editing * hide popover when drag/resize/rotate * unmount during autohide * autohide after 500ms * fix regression * prevent cmd/ctrl+k when inside link editor * submit when input not updated * allow custom urls * fix centering of popup when zoomed * fix hitbox during zoom * fix * tweak link normalization * touch hyperlink tooltip DOM only if needed * consider 0 if no offsetY * reduce hitbox of link icon and make sure link icon doesn't show on top of higher z-index elements * show link tooltip only if element has higher z-index * dnt show hyperlink popup when selection changes from element with link to element with no link and also hide popover when element type changes from selection to something else * lint: EOL * fix link icon tooltip positioning * open the link only when last pointer down and last pointer up hit the link hitbox * render tooltip after 300ms delay * ensure link popup and editor input have same height * wip: cache the link icon canvas * fix the image quality after caching using device pixel ratio yay * some cleanup * remove unused selectedElementIds from renderConfig * Update src/renderer/renderElement.ts * fix `opener` vulnerability * tweak styling * decrease padding * open local links in the same tab * fix caching * code style refactor * remove unnecessary save & restore * show link shortcut in help dialog * submit on cmd/ctrl+k * merge state props * Add title for link * update editview if prop changes * tweak link action logic * make `Hyperlink` compo editor state fully controlled * dont show popup when context menu open * show in contextMenu only for single selection & change pos * set button `selected` state * set contextMenuOpen on pointerdown * set contextMenyOpen to false when action triggered * don't render link icons on export * fix tests * fix buttons wrap * move focus states to input top-level rule * fix elements sharing `Hyperlink` state * fix hitbox for link icon in case of rect * Early return if hitting link icon Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
59cbf5fde5
commit
f47ddb988f
@ -81,3 +81,4 @@ export { actionToggleZenMode } from "./actionToggleZenMode";
|
||||
|
||||
export { actionToggleStats } from "./actionToggleStats";
|
||||
export { actionUnbindText } from "./actionUnbindText";
|
||||
export { actionLink } from "../element/Hyperlink";
|
||||
|
@ -25,7 +25,8 @@ export type ShortcutName =
|
||||
| "addToLibrary"
|
||||
| "viewMode"
|
||||
| "flipHorizontal"
|
||||
| "flipVertical";
|
||||
| "flipVertical"
|
||||
| "link";
|
||||
|
||||
const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
cut: [getShortcutKey("CtrlOrCmd+X")],
|
||||
@ -62,6 +63,7 @@ const shortcutMap: Record<ShortcutName, string[]> = {
|
||||
flipHorizontal: [getShortcutKey("Shift+H")],
|
||||
flipVertical: [getShortcutKey("Shift+V")],
|
||||
viewMode: [getShortcutKey("Alt+R")],
|
||||
link: [getShortcutKey("CtrlOrCmd+K")],
|
||||
};
|
||||
|
||||
export const getShortcutFromShortcutName = (name: ShortcutName) => {
|
||||
|
@ -104,7 +104,8 @@ export type ActionName =
|
||||
| "toggleTheme"
|
||||
| "increaseFontSize"
|
||||
| "decreaseFontSize"
|
||||
| "unbindText";
|
||||
| "unbindText"
|
||||
| "link";
|
||||
|
||||
export type PanelComponentProps = {
|
||||
elements: readonly ExcalidrawElement[];
|
||||
|
@ -84,6 +84,7 @@ export const getDefaultAppState = (): Omit<
|
||||
},
|
||||
viewModeEnabled: false,
|
||||
pendingImageElement: null,
|
||||
showHyperlinkPopup: false,
|
||||
};
|
||||
};
|
||||
|
||||
@ -174,6 +175,7 @@ const APP_STATE_STORAGE_CONF = (<
|
||||
zoom: { browser: true, export: false, server: false },
|
||||
viewModeEnabled: { browser: false, export: false, server: false },
|
||||
pendingImageElement: { browser: false, export: false, server: false },
|
||||
showHyperlinkPopup: { browser: false, export: false, server: false },
|
||||
});
|
||||
|
||||
const _clearAppStateForStorage = <
|
||||
|
@ -158,6 +158,7 @@ export const SelectedShapeActions = ({
|
||||
{renderAction("deleteSelectedElements")}
|
||||
{renderAction("group")}
|
||||
{renderAction("ungroup")}
|
||||
{targetElements.length === 1 && renderAction("link")}
|
||||
</div>
|
||||
</fieldset>
|
||||
)}
|
||||
|
@ -28,6 +28,7 @@ import {
|
||||
actionToggleZenMode,
|
||||
actionUnbindText,
|
||||
actionUngroup,
|
||||
actionLink,
|
||||
} from "../actions";
|
||||
import { createRedoAction, createUndoAction } from "../actions/actionHistory";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
@ -141,6 +142,7 @@ import {
|
||||
InitializedExcalidrawImageElement,
|
||||
ExcalidrawImageElement,
|
||||
FileId,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { getCenter, getDistance } from "../gesture";
|
||||
import {
|
||||
@ -239,6 +241,14 @@ import {
|
||||
getBoundTextElementId,
|
||||
} from "../element/textElement";
|
||||
import { isHittingElementNotConsideringBoundingBox } from "../element/collision";
|
||||
import {
|
||||
normalizeLink,
|
||||
showHyperlinkTooltip,
|
||||
hideHyperlinkToolip,
|
||||
Hyperlink,
|
||||
isPointHittingLinkIcon,
|
||||
isLocalLink,
|
||||
} from "../element/Hyperlink";
|
||||
|
||||
const IsMobileContext = React.createContext(false);
|
||||
export const useIsMobile = () => useContext(IsMobileContext);
|
||||
@ -298,6 +308,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
public files: BinaryFiles = {};
|
||||
public imageCache: AppClassProperties["imageCache"] = new Map();
|
||||
|
||||
hitLinkElement?: NonDeletedExcalidrawElement;
|
||||
lastPointerDown: React.PointerEvent<HTMLCanvasElement> | null = null;
|
||||
lastPointerUp: React.PointerEvent<HTMLElement> | PointerEvent | null = null;
|
||||
contextMenuOpen: boolean = false;
|
||||
|
||||
constructor(props: AppProps) {
|
||||
super(props);
|
||||
const defaultAppState = getDefaultAppState();
|
||||
@ -320,6 +335,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
name,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
showHyperlinkPopup: false,
|
||||
};
|
||||
|
||||
this.id = nanoid();
|
||||
@ -433,7 +449,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
public render() {
|
||||
const { zenModeEnabled, viewModeEnabled } = this.state;
|
||||
|
||||
const selectedElement = getSelectedElements(
|
||||
this.scene.getElements(),
|
||||
this.state,
|
||||
);
|
||||
const {
|
||||
onCollabButtonClick,
|
||||
renderTopRightUI,
|
||||
@ -499,6 +518,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
/>
|
||||
<div className="excalidraw-textEditorContainer" />
|
||||
<div className="excalidraw-contextMenuContainer" />
|
||||
{selectedElement.length === 1 && this.state.showHyperlinkPopup && (
|
||||
<Hyperlink
|
||||
key={selectedElement[0].id}
|
||||
element={selectedElement[0]}
|
||||
appState={this.state}
|
||||
setAppState={this.setAppState}
|
||||
/>
|
||||
)}
|
||||
{this.state.showStats && (
|
||||
<Stats
|
||||
appState={this.state}
|
||||
@ -537,6 +564,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
|
||||
private syncActionResult = withBatchedUpdates(
|
||||
(actionResult: ActionResult) => {
|
||||
// Since context menu closes when action triggered so setting to false
|
||||
this.contextMenuOpen = false;
|
||||
if (this.unmounted || actionResult === false) {
|
||||
return;
|
||||
}
|
||||
@ -1012,6 +1041,14 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: AppProps, prevState: AppState) {
|
||||
// Hide hyperlink popup if shown when element type is not selection
|
||||
if (
|
||||
prevState.elementType === "selection" &&
|
||||
this.state.elementType !== "selection" &&
|
||||
this.state.showHyperlinkPopup
|
||||
) {
|
||||
this.setState({ showHyperlinkPopup: false });
|
||||
}
|
||||
if (prevProps.langCode !== this.props.langCode) {
|
||||
this.updateLanguage();
|
||||
}
|
||||
@ -1157,6 +1194,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
renderScrollbars: !this.isMobile,
|
||||
},
|
||||
);
|
||||
|
||||
if (scrollBars) {
|
||||
currentScrollBars = scrollBars;
|
||||
}
|
||||
@ -1481,6 +1519,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
};
|
||||
|
||||
removePointer = (event: React.PointerEvent<HTMLElement> | PointerEvent) => {
|
||||
this.lastPointerUp = event;
|
||||
// remove touch handler for context menu on touch devices
|
||||
if (event.pointerType === "touch" && touchTimeout) {
|
||||
clearTimeout(touchTimeout);
|
||||
@ -2083,6 +2122,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
.filter(
|
||||
(element) => !(isTextElement(element) && element.containerId),
|
||||
);
|
||||
|
||||
return getElementsAtPosition(elements, (element) =>
|
||||
hitTest(element, this.state, x, y),
|
||||
);
|
||||
@ -2308,6 +2348,69 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
};
|
||||
|
||||
private getElementLinkAtPosition = (
|
||||
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.getElements().slice().reverse();
|
||||
let hitElementIndex = Infinity;
|
||||
|
||||
return elements.find((element, index) => {
|
||||
if (hitElement && element.id === hitElement.id) {
|
||||
hitElementIndex = index;
|
||||
}
|
||||
return (
|
||||
element.link &&
|
||||
isPointHittingLinkIcon(element, this.state, [
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
]) &&
|
||||
index <= hitElementIndex
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
private redirectToLink = () => {
|
||||
const lastPointerDownCoords = viewportCoordsToSceneCoords(
|
||||
this.lastPointerDown!,
|
||||
this.state,
|
||||
);
|
||||
const lastPointerDownHittingLinkIcon = isPointHittingLinkIcon(
|
||||
this.hitLinkElement!,
|
||||
this.state,
|
||||
[lastPointerDownCoords.x, lastPointerDownCoords.y],
|
||||
);
|
||||
const lastPointerUpCoords = viewportCoordsToSceneCoords(
|
||||
this.lastPointerUp!,
|
||||
this.state,
|
||||
);
|
||||
const LastPointerUpHittingLinkIcon = isPointHittingLinkIcon(
|
||||
this.hitLinkElement!,
|
||||
this.state,
|
||||
[lastPointerUpCoords.x, lastPointerUpCoords.y],
|
||||
);
|
||||
if (lastPointerDownHittingLinkIcon && LastPointerUpHittingLinkIcon) {
|
||||
const url = this.hitLinkElement?.link;
|
||||
if (url) {
|
||||
const target = isLocalLink(url) ? "_self" : "_blank";
|
||||
const newWindow = window.open(undefined, target);
|
||||
// https://mathiasbynens.github.io/rel-noopener/
|
||||
if (newWindow) {
|
||||
newWindow.opener = null;
|
||||
newWindow.location = normalizeLink(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
private attachLinkListener = () => {
|
||||
this.canvas?.addEventListener("click", this.redirectToLink);
|
||||
};
|
||||
private detachLinkListener = () => {
|
||||
this.canvas?.removeEventListener("click", this.redirectToLink);
|
||||
};
|
||||
|
||||
private handleCanvasPointerMove = (
|
||||
event: React.PointerEvent<HTMLCanvasElement>,
|
||||
) => {
|
||||
@ -2540,42 +2643,68 @@ class App extends React.Component<AppProps, AppState> {
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
);
|
||||
if (this.state.elementType === "text") {
|
||||
setCursor(
|
||||
this.canvas,
|
||||
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
|
||||
);
|
||||
} else if (this.state.viewModeEnabled) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
||||
} else if (isOverScrollBar) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||
} else if (this.state.editingLinearElement) {
|
||||
const element = LinearElementEditor.getElement(
|
||||
this.state.editingLinearElement.elementId,
|
||||
);
|
||||
this.hitLinkElement = this.getElementLinkAtPosition(
|
||||
scenePointer,
|
||||
hitElement,
|
||||
);
|
||||
|
||||
if (
|
||||
this.hitLinkElement &&
|
||||
!this.state.selectedElementIds[this.hitLinkElement.id]
|
||||
) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
||||
showHyperlinkTooltip(this.hitLinkElement, this.state);
|
||||
this.attachLinkListener();
|
||||
} else {
|
||||
hideHyperlinkToolip();
|
||||
this.detachLinkListener();
|
||||
if (
|
||||
element &&
|
||||
isHittingElementNotConsideringBoundingBox(element, this.state, [
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
])
|
||||
hitElement &&
|
||||
hitElement.link &&
|
||||
this.state.selectedElementIds[hitElement.id] &&
|
||||
!this.contextMenuOpen &&
|
||||
!this.state.showHyperlinkPopup
|
||||
) {
|
||||
this.setState({ showHyperlinkPopup: "info" });
|
||||
}
|
||||
if (this.state.elementType === "text") {
|
||||
setCursor(
|
||||
this.canvas,
|
||||
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
|
||||
);
|
||||
} else if (this.state.viewModeEnabled) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
||||
} else if (isOverScrollBar) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||
} else if (this.state.editingLinearElement) {
|
||||
const element = LinearElementEditor.getElement(
|
||||
this.state.editingLinearElement.elementId,
|
||||
);
|
||||
|
||||
if (
|
||||
element &&
|
||||
isHittingElementNotConsideringBoundingBox(element, this.state, [
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
])
|
||||
) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
||||
} else {
|
||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||
}
|
||||
} else if (
|
||||
// if using cmd/ctrl, we're not dragging
|
||||
!event[KEYS.CTRL_OR_CMD] &&
|
||||
(hitElement ||
|
||||
this.isHittingCommonBoundingBoxOfSelectedElements(
|
||||
scenePointer,
|
||||
selectedElements,
|
||||
))
|
||||
) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
||||
} else {
|
||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||
}
|
||||
} else if (
|
||||
// if using cmd/ctrl, we're not dragging
|
||||
!event[KEYS.CTRL_OR_CMD] &&
|
||||
(hitElement ||
|
||||
this.isHittingCommonBoundingBoxOfSelectedElements(
|
||||
scenePointer,
|
||||
selectedElements,
|
||||
))
|
||||
) {
|
||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
||||
} else {
|
||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
||||
}
|
||||
};
|
||||
|
||||
@ -2594,7 +2723,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (selection?.anchorNode) {
|
||||
selection.removeAllRanges();
|
||||
}
|
||||
|
||||
this.maybeOpenContextMenuAfterPointerDownOnTouchDevices(event);
|
||||
this.maybeCleanupAfterMissingPointerUp(event);
|
||||
|
||||
@ -2612,7 +2740,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
if (isPanning) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastPointerDown = event;
|
||||
this.setState({
|
||||
lastPointerDownWith: event.pointerType,
|
||||
cursorButton: "down",
|
||||
@ -2646,6 +2774,8 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Since context menu closes on pointer down so setting to false
|
||||
this.contextMenuOpen = false;
|
||||
this.clearSelectionIfNotUsingSelection();
|
||||
this.updateBindingEnabledOnPointerMove(event);
|
||||
|
||||
@ -3072,7 +3202,6 @@ class App extends React.Component<AppProps, AppState> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// hitElement may already be set above, so check first
|
||||
pointerDownState.hit.element =
|
||||
pointerDownState.hit.element ??
|
||||
@ -3082,6 +3211,15 @@ class App extends React.Component<AppProps, AppState> {
|
||||
);
|
||||
|
||||
if (pointerDownState.hit.element) {
|
||||
// Early return if pointer is hitting link icon
|
||||
if (
|
||||
isPointHittingLinkIcon(pointerDownState.hit.element, this.state, [
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
pointerDownState.hit.hasHitElementInside =
|
||||
isHittingElementNotConsideringBoundingBox(
|
||||
pointerDownState.hit.element,
|
||||
@ -3163,6 +3301,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
...prevState.selectedElementIds,
|
||||
[hitElement.id]: true,
|
||||
},
|
||||
showHyperlinkPopup: hitElement.link ? "info" : false,
|
||||
},
|
||||
this.scene.getElements(),
|
||||
);
|
||||
@ -3819,6 +3958,11 @@ class App extends React.Component<AppProps, AppState> {
|
||||
}
|
||||
: null),
|
||||
},
|
||||
showHyperlinkPopup:
|
||||
elementsWithinSelection.length === 1 &&
|
||||
elementsWithinSelection[0].link
|
||||
? "info"
|
||||
: false,
|
||||
},
|
||||
this.scene.getElements(),
|
||||
),
|
||||
@ -4970,6 +5114,10 @@ class App extends React.Component<AppProps, AppState> {
|
||||
},
|
||||
type: "canvas" | "element",
|
||||
) => {
|
||||
if (this.state.showHyperlinkPopup) {
|
||||
this.setState({ showHyperlinkPopup: false });
|
||||
}
|
||||
this.contextMenuOpen = true;
|
||||
const maybeGroupAction = actionGroup.contextItemPredicate!(
|
||||
this.actionManager.getElementsIncludingDeleted(),
|
||||
this.actionManager.getAppState(),
|
||||
@ -5116,6 +5264,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
maybeFlipHorizontal && actionFlipHorizontal,
|
||||
maybeFlipVertical && actionFlipVertical,
|
||||
(maybeFlipHorizontal || maybeFlipVertical) && separator,
|
||||
actionLink.contextItemPredicate(elements, this.state) && actionLink,
|
||||
actionDuplicateSelection,
|
||||
actionDeleteSelected,
|
||||
],
|
||||
|
@ -205,6 +205,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => {
|
||||
label={t("helpDialog.preventBinding")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd")]}
|
||||
/>
|
||||
<Shortcut
|
||||
label={t("toolBar.link")}
|
||||
shortcuts={[getShortcutKey("CtrlOrCmd+K")]}
|
||||
/>
|
||||
</ShortcutIsland>
|
||||
<ShortcutIsland caption={t("helpDialog.view")}>
|
||||
<Shortcut
|
||||
|
@ -2,7 +2,7 @@ import "./Tooltip.scss";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
const getTooltipDiv = () => {
|
||||
export const getTooltipDiv = () => {
|
||||
const existingDiv = document.querySelector<HTMLDivElement>(
|
||||
".excalidraw-tooltip",
|
||||
);
|
||||
@ -15,6 +15,50 @@ const getTooltipDiv = () => {
|
||||
return div;
|
||||
};
|
||||
|
||||
export const updateTooltipPosition = (
|
||||
tooltip: HTMLDivElement,
|
||||
item: {
|
||||
left: number;
|
||||
top: number;
|
||||
width: number;
|
||||
height: number;
|
||||
},
|
||||
position: "bottom" | "top" = "bottom",
|
||||
) => {
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
const margin = 5;
|
||||
|
||||
let left = item.left + item.width / 2 - tooltipRect.width / 2;
|
||||
if (left < 0) {
|
||||
left = margin;
|
||||
} else if (left + tooltipRect.width >= viewportWidth) {
|
||||
left = viewportWidth - tooltipRect.width - margin;
|
||||
}
|
||||
|
||||
let top: number;
|
||||
|
||||
if (position === "bottom") {
|
||||
top = item.top + item.height + margin;
|
||||
if (top + tooltipRect.height >= viewportHeight) {
|
||||
top = item.top - tooltipRect.height - margin;
|
||||
}
|
||||
} else {
|
||||
top = item.top - tooltipRect.height - margin;
|
||||
if (top < 0) {
|
||||
top = item.top + item.height + margin;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(tooltip.style, {
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
});
|
||||
};
|
||||
|
||||
const updateTooltip = (
|
||||
item: HTMLDivElement,
|
||||
tooltip: HTMLDivElement,
|
||||
@ -27,35 +71,8 @@ const updateTooltip = (
|
||||
|
||||
tooltip.textContent = label;
|
||||
|
||||
const {
|
||||
x: itemX,
|
||||
bottom: itemBottom,
|
||||
top: itemTop,
|
||||
width: itemWidth,
|
||||
} = item.getBoundingClientRect();
|
||||
|
||||
const { width: labelWidth, height: labelHeight } =
|
||||
tooltip.getBoundingClientRect();
|
||||
|
||||
const viewportWidth = window.innerWidth;
|
||||
const viewportHeight = window.innerHeight;
|
||||
|
||||
const margin = 5;
|
||||
|
||||
const left = itemX + itemWidth / 2 - labelWidth / 2;
|
||||
const offsetLeft =
|
||||
left + labelWidth >= viewportWidth ? left + labelWidth - viewportWidth : 0;
|
||||
|
||||
const top = itemBottom + margin;
|
||||
const offsetTop =
|
||||
top + labelHeight >= viewportHeight
|
||||
? itemBottom - itemTop + labelHeight + margin * 2
|
||||
: 0;
|
||||
|
||||
Object.assign(tooltip.style, {
|
||||
top: `${top - offsetTop}px`,
|
||||
left: `${left - offsetLeft}px`,
|
||||
});
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
updateTooltipPosition(tooltip, itemRect);
|
||||
};
|
||||
|
||||
type TooltipProps = {
|
||||
@ -75,7 +92,6 @@ export const Tooltip = ({
|
||||
return () =>
|
||||
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="excalidraw-tooltip-wrapper"
|
||||
|
@ -892,3 +892,11 @@ export const publishIcon = createIcon(
|
||||
/>,
|
||||
{ width: 640, height: 512 },
|
||||
);
|
||||
|
||||
export const editIcon = createIcon(
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M402.3 344.9l32-32c5-5 13.7-1.5 13.7 5.7V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h273.5c7.1 0 10.7 8.6 5.7 13.7l-32 32c-1.5 1.5-3.5 2.3-5.7 2.3H48v352h352V350.5c0-2.1.8-4.1 2.3-5.6zm156.6-201.8L296.3 405.7l-90.4 10c-26.2 2.9-48.5-19.2-45.6-45.6l10-90.4L432.9 17.1c22.9-22.9 59.9-22.9 82.7 0l43.2 43.2c22.9 22.9 22.9 60 .1 82.8zM460.1 174L402 115.9 216.2 301.8l-7.3 65.3 65.3-7.3L460.1 174zm64.8-79.7l-43.2-43.2c-4.1-4.1-10.8-4.1-14.8 0L436 82l58.1 58.1 30.9-30.9c4-4.2 4-10.8-.1-14.9z"
|
||||
></path>,
|
||||
{ width: 640, height: 512 },
|
||||
);
|
||||
|
@ -115,6 +115,7 @@ export const TOAST_TIMEOUT = 5000;
|
||||
export const VERSION_TIMEOUT = 30000;
|
||||
export const SCROLL_TIMEOUT = 100;
|
||||
export const ZOOM_STEP = 0.1;
|
||||
export const HYPERLINK_TOOLTIP_DELAY = 300;
|
||||
|
||||
// Report a user inactive after IDLE_THRESHOLD milliseconds
|
||||
export const IDLE_THRESHOLD = 60_000;
|
||||
|
@ -105,6 +105,7 @@ const restoreElementWithProperties = <
|
||||
? element.boundElementIds.map((id) => ({ type: "arrow", id }))
|
||||
: element.boundElements ?? [],
|
||||
updated: element.updated ?? getUpdatedTimestamp(),
|
||||
link: element.link ?? null,
|
||||
};
|
||||
|
||||
return {
|
||||
|
74
src/element/Hyperlink.scss
Normal file
74
src/element/Hyperlink.scss
Normal file
@ -0,0 +1,74 @@
|
||||
@import "../css/variables.module";
|
||||
|
||||
.excalidraw-hyperlinkContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
position: absolute;
|
||||
box-shadow: 0px 2px 4px 0 rgb(0 0 0 / 30%);
|
||||
z-index: 100;
|
||||
background: var(--island-bg-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
box-sizing: border-box;
|
||||
// to account for LS due to rendering icons after new link created
|
||||
min-height: 42px;
|
||||
|
||||
&-input,
|
||||
button {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
&-input,
|
||||
&-link {
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
line-height: 24px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
font-family: var(--ui-font);
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 18rem;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
color: var(--text-primary-color);
|
||||
|
||||
outline: none;
|
||||
border: none;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
&-link {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 15rem;
|
||||
}
|
||||
|
||||
button {
|
||||
color: $oc-blue-6;
|
||||
background-color: transparent !important;
|
||||
font-weight: 500;
|
||||
&.excalidraw-hyperlinkContainer--remove {
|
||||
color: $oc-red-9;
|
||||
}
|
||||
}
|
||||
|
||||
.d-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--remove .ToolIcon__icon svg {
|
||||
color: $oc-red-6;
|
||||
}
|
||||
|
||||
.ToolIcon__icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
&__buttons {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
429
src/element/Hyperlink.tsx
Normal file
429
src/element/Hyperlink.tsx
Normal file
@ -0,0 +1,429 @@
|
||||
import { AppState, Point } from "../types";
|
||||
import {
|
||||
getShortcutKey,
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
} from "../utils";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { NonDeletedExcalidrawElement } from "./types";
|
||||
|
||||
import { register } from "../actions/register";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { editIcon, link, trash } from "../components/icons";
|
||||
import { t } from "../i18n";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import clsx from "clsx";
|
||||
import { KEYS } from "../keys";
|
||||
import { DEFAULT_LINK_SIZE } from "../renderer/renderElement";
|
||||
import { rotate } from "../math";
|
||||
import { EVENT, HYPERLINK_TOOLTIP_DELAY, MIME_TYPES } from "../constants";
|
||||
import { Bounds } from "./bounds";
|
||||
import { getTooltipDiv, updateTooltipPosition } from "../components/Tooltip";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { isPointHittingElementBoundingBox } from "./collision";
|
||||
import { getElementAbsoluteCoords } from "./";
|
||||
|
||||
import "./Hyperlink.scss";
|
||||
|
||||
const CONTAINER_WIDTH = 320;
|
||||
const SPACE_BOTTOM = 85;
|
||||
const CONTAINER_PADDING = 5;
|
||||
const CONTAINER_HEIGHT = 42;
|
||||
const AUTO_HIDE_TIMEOUT = 500;
|
||||
|
||||
export const EXTERNAL_LINK_IMG = document.createElement("img");
|
||||
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>`,
|
||||
)}`;
|
||||
|
||||
let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
|
||||
|
||||
export const Hyperlink = ({
|
||||
element,
|
||||
appState,
|
||||
setAppState,
|
||||
}: {
|
||||
element: NonDeletedExcalidrawElement;
|
||||
appState: AppState;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
}) => {
|
||||
const linkVal = element.link || "";
|
||||
|
||||
const [inputVal, setInputVal] = useState(linkVal);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const isEditing = appState.showHyperlinkPopup === "editor" || !linkVal;
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!inputRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const link = normalizeLink(inputRef.current.value);
|
||||
|
||||
mutateElement(element, { link });
|
||||
setAppState({ showHyperlinkPopup: "info" });
|
||||
}, [element, setAppState]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
return () => {
|
||||
handleSubmit();
|
||||
};
|
||||
}, [handleSubmit]);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: number | null = null;
|
||||
const handlePointerMove = (event: PointerEvent) => {
|
||||
if (isEditing) {
|
||||
return;
|
||||
}
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
const shouldHide = shouldHideLinkPopup(element, appState, [
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
]) as boolean;
|
||||
if (shouldHide) {
|
||||
timeoutId = window.setTimeout(() => {
|
||||
setAppState({ showHyperlinkPopup: false });
|
||||
}, AUTO_HIDE_TIMEOUT);
|
||||
}
|
||||
};
|
||||
window.addEventListener(EVENT.POINTER_MOVE, handlePointerMove, false);
|
||||
return () => {
|
||||
window.removeEventListener(EVENT.POINTER_MOVE, handlePointerMove, false);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}, [appState, element, isEditing, setAppState]);
|
||||
|
||||
const handleRemove = useCallback(() => {
|
||||
mutateElement(element, { link: null });
|
||||
if (isEditing) {
|
||||
inputRef.current!.value = "";
|
||||
}
|
||||
setAppState({ showHyperlinkPopup: false });
|
||||
}, [setAppState, element, isEditing]);
|
||||
|
||||
const onEdit = () => {
|
||||
setAppState({ showHyperlinkPopup: "editor" });
|
||||
};
|
||||
const { x, y } = getCoordsForPopover(element, appState);
|
||||
if (
|
||||
appState.draggingElement ||
|
||||
appState.resizingElement ||
|
||||
appState.isRotating
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="excalidraw-hyperlinkContainer"
|
||||
style={{
|
||||
top: `${y}px`,
|
||||
left: `${x}px`,
|
||||
width: CONTAINER_WIDTH,
|
||||
padding: CONTAINER_PADDING,
|
||||
}}
|
||||
>
|
||||
{isEditing ? (
|
||||
<input
|
||||
className={clsx("excalidraw-hyperlinkContainer-input")}
|
||||
placeholder="Type or paste your link here"
|
||||
ref={inputRef}
|
||||
value={inputVal}
|
||||
onChange={(event) => setInputVal(event.target.value)}
|
||||
autoFocus
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
// prevent cmd/ctrl+k shortcut when editing link
|
||||
if (event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (event.key === KEYS.ENTER || event.key === KEYS.ESCAPE) {
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<a
|
||||
href={element.link || ""}
|
||||
className={clsx("excalidraw-hyperlinkContainer-link", {
|
||||
"d-none": isEditing,
|
||||
})}
|
||||
target={isLocalLink(element.link) ? "_self" : "_blank"}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{element.link}
|
||||
</a>
|
||||
)}
|
||||
<div className="excalidraw-hyperlinkContainer__buttons">
|
||||
{!isEditing && (
|
||||
<ToolButton
|
||||
type="button"
|
||||
title={t("buttons.edit")}
|
||||
aria-label={t("buttons.edit")}
|
||||
label={t("buttons.edit")}
|
||||
onClick={onEdit}
|
||||
className="excalidraw-hyperlinkContainer--edit"
|
||||
icon={editIcon}
|
||||
/>
|
||||
)}
|
||||
|
||||
{linkVal && (
|
||||
<ToolButton
|
||||
type="button"
|
||||
title={t("buttons.remove")}
|
||||
aria-label={t("buttons.remove")}
|
||||
label={t("buttons.remove")}
|
||||
onClick={handleRemove}
|
||||
className="excalidraw-hyperlinkContainer--remove"
|
||||
icon={trash}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getCoordsForPopover = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
appState: AppState,
|
||||
) => {
|
||||
const { x: viewPortX, y: viewPortY } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: element.x + element.width / 2, sceneY: element.y },
|
||||
appState,
|
||||
);
|
||||
const x = viewPortX - CONTAINER_WIDTH / 2;
|
||||
const y = viewPortY - SPACE_BOTTOM;
|
||||
return { x, y };
|
||||
};
|
||||
|
||||
export const normalizeLink = (link: string) => {
|
||||
link = link.trim();
|
||||
if (link) {
|
||||
// prefix with protocol if not fully-qualified
|
||||
if (!link.includes("://") && !/^[[\\/]/.test(link)) {
|
||||
link = `https://${link}`;
|
||||
}
|
||||
}
|
||||
return link;
|
||||
};
|
||||
|
||||
export const isLocalLink = (link: string | null) => {
|
||||
return !!(link?.includes(location.origin) || link?.startsWith("/"));
|
||||
};
|
||||
|
||||
export const actionLink = register({
|
||||
name: "link",
|
||||
perform: (elements, appState) => {
|
||||
if (appState.showHyperlinkPopup === "editor") {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
elements,
|
||||
appState: {
|
||||
...appState,
|
||||
showHyperlinkPopup: "editor",
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
|
||||
contextItemLabel: (elements, appState) =>
|
||||
getContextMenuLabel(elements, appState),
|
||||
contextItemPredicate: (elements, appState) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
return selectedElements.length === 1;
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
|
||||
return (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={link}
|
||||
aria-label={t(getContextMenuLabel(elements, appState))}
|
||||
title={`${t("labels.link.label")} - ${getShortcutKey("CtrlOrCmd+K")}`}
|
||||
onClick={() => updateData(null)}
|
||||
selected={selectedElements.length === 1 && !!selectedElements[0].link}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
export const getContextMenuLabel = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
) => {
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
const label = selectedElements[0]!.link
|
||||
? "labels.link.edit"
|
||||
: "labels.link.create";
|
||||
return label;
|
||||
};
|
||||
export const getLinkHandleFromCoords = (
|
||||
[x1, y1, x2, y2]: Bounds,
|
||||
angle: number,
|
||||
appState: AppState,
|
||||
): [x: number, y: number, width: number, height: number] => {
|
||||
const size = DEFAULT_LINK_SIZE;
|
||||
const linkWidth = size / appState.zoom.value;
|
||||
const linkHeight = size / appState.zoom.value;
|
||||
const linkMarginY = size / appState.zoom.value;
|
||||
const centerX = (x1 + x2) / 2;
|
||||
const centerY = (y1 + y2) / 2;
|
||||
const centeringOffset = (size - 8) / (2 * appState.zoom.value);
|
||||
const dashedLineMargin = 4 / appState.zoom.value;
|
||||
|
||||
// Same as `ne` resize handle
|
||||
const x = x2 + dashedLineMargin - centeringOffset;
|
||||
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
|
||||
|
||||
const [rotatedX, rotatedY] = rotate(
|
||||
x + linkWidth / 2,
|
||||
y + linkHeight / 2,
|
||||
centerX,
|
||||
centerY,
|
||||
angle,
|
||||
);
|
||||
return [
|
||||
rotatedX - linkWidth / 2,
|
||||
rotatedY - linkHeight / 2,
|
||||
linkWidth,
|
||||
linkHeight,
|
||||
];
|
||||
};
|
||||
|
||||
export const isPointHittingLinkIcon = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
appState: AppState,
|
||||
[x, y]: Point,
|
||||
) => {
|
||||
const threshold = 4 / appState.zoom.value;
|
||||
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
|
||||
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
|
||||
[x1, y1, x2, y2],
|
||||
element.angle,
|
||||
appState,
|
||||
);
|
||||
const hitLink =
|
||||
x > linkX - threshold &&
|
||||
x < linkX + threshold + linkWidth &&
|
||||
y > linkY - threshold &&
|
||||
y < linkY + linkHeight + threshold;
|
||||
|
||||
return hitLink;
|
||||
};
|
||||
|
||||
let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null;
|
||||
export const showHyperlinkTooltip = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
appState: AppState,
|
||||
) => {
|
||||
if (HYPERLINK_TOOLTIP_TIMEOUT_ID) {
|
||||
clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID);
|
||||
}
|
||||
HYPERLINK_TOOLTIP_TIMEOUT_ID = window.setTimeout(
|
||||
() => renderTooltip(element, appState),
|
||||
HYPERLINK_TOOLTIP_DELAY,
|
||||
);
|
||||
};
|
||||
|
||||
const renderTooltip = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
appState: AppState,
|
||||
) => {
|
||||
if (!element.link) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tooltipDiv = getTooltipDiv();
|
||||
|
||||
tooltipDiv.classList.add("excalidraw-tooltip--visible");
|
||||
tooltipDiv.style.maxWidth = "20rem";
|
||||
tooltipDiv.textContent = element.link;
|
||||
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
|
||||
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
|
||||
[x1, y1, x2, y2],
|
||||
element.angle,
|
||||
appState,
|
||||
);
|
||||
|
||||
const linkViewportCoords = sceneCoordsToViewportCoords(
|
||||
{ sceneX: linkX, sceneY: linkY },
|
||||
appState,
|
||||
);
|
||||
|
||||
updateTooltipPosition(
|
||||
tooltipDiv,
|
||||
{
|
||||
left: linkViewportCoords.x,
|
||||
top: linkViewportCoords.y,
|
||||
width: linkWidth,
|
||||
height: linkHeight,
|
||||
},
|
||||
"top",
|
||||
);
|
||||
|
||||
IS_HYPERLINK_TOOLTIP_VISIBLE = true;
|
||||
};
|
||||
export const hideHyperlinkToolip = () => {
|
||||
if (HYPERLINK_TOOLTIP_TIMEOUT_ID) {
|
||||
clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID);
|
||||
}
|
||||
if (IS_HYPERLINK_TOOLTIP_VISIBLE) {
|
||||
IS_HYPERLINK_TOOLTIP_VISIBLE = false;
|
||||
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
|
||||
}
|
||||
};
|
||||
|
||||
export const shouldHideLinkPopup = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
appState: AppState,
|
||||
[clientX, clientY]: Point,
|
||||
): Boolean => {
|
||||
const { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||
{ clientX, clientY },
|
||||
appState,
|
||||
);
|
||||
|
||||
const threshold = 15 / appState.zoom.value;
|
||||
// hitbox to prevent hiding when hovered in element bounding box
|
||||
if (isPointHittingElementBoundingBox(element, [sceneX, sceneY], threshold)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// hit box to prevent hiding when hovered in the vertical area between element and popover
|
||||
if (
|
||||
sceneX >= element.x &&
|
||||
sceneX <= element.x + element.width &&
|
||||
sceneY <= element.y &&
|
||||
sceneY >= element.y - SPACE_BOTTOM
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// hit box to prevent hiding when hovered around popover within threshold
|
||||
const { x: popoverX, y: popoverY } = getCoordsForPopover(element, appState);
|
||||
|
||||
if (
|
||||
clientX >= popoverX - threshold &&
|
||||
clientX <= popoverX + CONTAINER_WIDTH + CONTAINER_PADDING * 2 + threshold &&
|
||||
clientY >= popoverY - threshold &&
|
||||
clientY <= popoverY + threshold + CONTAINER_PADDING * 2 + CONTAINER_HEIGHT
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
@ -96,7 +96,6 @@ export const isHittingElementNotConsideringBoundingBox = (
|
||||
: isElementDraggableFromInside(element)
|
||||
? isInsideCheck
|
||||
: isNearCheck;
|
||||
|
||||
return hitTestPointAgainstElement({ element, point, threshold, check });
|
||||
};
|
||||
|
||||
@ -105,7 +104,7 @@ const isElementSelected = (
|
||||
element: NonDeleted<ExcalidrawElement>,
|
||||
) => appState.selectedElementIds[element.id];
|
||||
|
||||
const isPointHittingElementBoundingBox = (
|
||||
export const isPointHittingElementBoundingBox = (
|
||||
element: NonDeleted<ExcalidrawElement>,
|
||||
[x, y]: Point,
|
||||
threshold: number,
|
||||
|
@ -35,6 +35,7 @@ type ElementConstructorOpts = MarkOptional<
|
||||
| "seed"
|
||||
| "version"
|
||||
| "versionNonce"
|
||||
| "link"
|
||||
>;
|
||||
|
||||
const _newElementBase = <T extends ExcalidrawElement>(
|
||||
@ -55,6 +56,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
||||
groupIds = [],
|
||||
strokeSharpness,
|
||||
boundElements = null,
|
||||
link = null,
|
||||
...rest
|
||||
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
|
||||
) => {
|
||||
@ -81,6 +83,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
|
||||
isDeleted: false as false,
|
||||
boundElements,
|
||||
updated: getUpdatedTimestamp(),
|
||||
link,
|
||||
};
|
||||
return element;
|
||||
};
|
||||
|
@ -52,6 +52,7 @@ type _ExcalidrawElementBase = Readonly<{
|
||||
| null;
|
||||
/** epoch (ms) timestamp of last element update */
|
||||
updated: number;
|
||||
link: string | null;
|
||||
}>;
|
||||
|
||||
export type ExcalidrawSelectionElement = _ExcalidrawElementBase & {
|
||||
|
@ -61,6 +61,7 @@ export const KEYS = {
|
||||
X: "x",
|
||||
Y: "y",
|
||||
Z: "z",
|
||||
K: "k",
|
||||
} as const;
|
||||
|
||||
export type Key = keyof typeof KEYS;
|
||||
|
@ -105,7 +105,12 @@
|
||||
"excalidrawLib": "Excalidraw Library",
|
||||
"decreaseFontSize": "Decrease font size",
|
||||
"increaseFontSize": "Increase font size",
|
||||
"unbindText": "Unbind text"
|
||||
"unbindText": "Unbind text",
|
||||
"link": {
|
||||
"edit": "Edit link",
|
||||
"create": "Create link",
|
||||
"label": "Link"
|
||||
}
|
||||
},
|
||||
"buttons": {
|
||||
"clearReset": "Reset the canvas",
|
||||
@ -188,7 +193,8 @@
|
||||
"text": "Text",
|
||||
"library": "Library",
|
||||
"lock": "Keep selected tool active after drawing",
|
||||
"penMode": "Prevent pinch-zoom and accept freedraw input only from pen"
|
||||
"penMode": "Prevent pinch-zoom and accept freedraw input only from pen",
|
||||
"link": "Add/ Update link for a selected shape"
|
||||
},
|
||||
"headings": {
|
||||
"canvasActions": "Canvas actions",
|
||||
|
@ -145,6 +145,8 @@ const generateElementCanvas = (
|
||||
};
|
||||
};
|
||||
|
||||
export const DEFAULT_LINK_SIZE = 14;
|
||||
|
||||
const IMAGE_PLACEHOLDER_IMG = document.createElement("img");
|
||||
IMAGE_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
||||
`<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="image" class="svg-inline--fa fa-image fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#888" d="M464 448H48c-26.51 0-48-21.49-48-48V112c0-26.51 21.49-48 48-48h416c26.51 0 48 21.49 48 48v288c0 26.51-21.49 48-48 48zM112 120c-30.928 0-56 25.072-56 56s25.072 56 56 56 56-25.072 56-56-25.072-56-56-56zM64 384h384V272l-87.515-87.515c-4.686-4.686-12.284-4.686-16.971 0L208 320l-55.515-55.515c-4.686-4.686-12.284-4.686-16.971 0L64 336v48z"></path></svg>`,
|
||||
@ -410,23 +412,23 @@ const generateElementShape = (
|
||||
topY + (rightY - topY) * 0.25
|
||||
} L ${rightX - (rightX - topX) * 0.25} ${
|
||||
rightY - (rightY - topY) * 0.25
|
||||
}
|
||||
}
|
||||
C ${rightX} ${rightY}, ${rightX} ${rightY}, ${
|
||||
rightX - (rightX - bottomX) * 0.25
|
||||
} ${rightY + (bottomY - rightY) * 0.25}
|
||||
} ${rightY + (bottomY - rightY) * 0.25}
|
||||
L ${bottomX + (rightX - bottomX) * 0.25} ${
|
||||
bottomY - (bottomY - rightY) * 0.25
|
||||
}
|
||||
}
|
||||
C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${
|
||||
bottomX - (bottomX - leftX) * 0.25
|
||||
} ${bottomY - (bottomY - leftY) * 0.25}
|
||||
} ${bottomY - (bottomY - leftY) * 0.25}
|
||||
L ${leftX + (bottomX - leftX) * 0.25} ${
|
||||
leftY + (bottomY - leftY) * 0.25
|
||||
}
|
||||
}
|
||||
C ${leftX} ${leftY}, ${leftX} ${leftY}, ${
|
||||
leftX + (topX - leftX) * 0.25
|
||||
} ${leftY - (leftY - topY) * 0.25}
|
||||
L ${topX - (topX - leftX) * 0.25} ${topY + (leftY - topY) * 0.25}
|
||||
} ${leftY - (leftY - topY) * 0.25}
|
||||
L ${topX - (topX - leftX) * 0.25} ${topY + (leftY - topY) * 0.25}
|
||||
C ${topX} ${topY}, ${topX} ${topY}, ${
|
||||
topX + (rightX - topX) * 0.25
|
||||
} ${topY + (rightY - topY) * 0.25}`,
|
||||
|
@ -50,6 +50,10 @@ import {
|
||||
import { viewportCoordsToSceneCoords, supportsEmoji } from "../utils";
|
||||
import { UserIdleState } from "../types";
|
||||
import { THEME_FILTER } from "../constants";
|
||||
import {
|
||||
EXTERNAL_LINK_IMG,
|
||||
getLinkHandleFromCoords,
|
||||
} from "../element/Hyperlink";
|
||||
|
||||
const hasEmojiSupport = supportsEmoji();
|
||||
|
||||
@ -260,6 +264,9 @@ export const renderScene = (
|
||||
visibleElements.forEach((element) => {
|
||||
try {
|
||||
renderElement(element, rc, context, renderConfig);
|
||||
if (!isExporting) {
|
||||
renderLinkIcon(element, context, appState);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
}
|
||||
@ -740,6 +747,61 @@ const renderBindingHighlightForSuggestedPointBinding = (
|
||||
});
|
||||
};
|
||||
|
||||
let linkCanvasCache: any;
|
||||
const renderLinkIcon = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
context: CanvasRenderingContext2D,
|
||||
appState: AppState,
|
||||
) => {
|
||||
if (element.link && !appState.selectedElementIds[element.id]) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const [x, y, width, height] = getLinkHandleFromCoords(
|
||||
[x1, y1, x2, y2],
|
||||
element.angle,
|
||||
appState,
|
||||
);
|
||||
const centerX = x + width / 2;
|
||||
const centerY = y + height / 2;
|
||||
context.save();
|
||||
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 =
|
||||
height * window.devicePixelRatio * appState.zoom.value;
|
||||
const linkCanvasCacheContext = linkCanvasCache.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);
|
||||
linkCanvasCacheContext.restore();
|
||||
context.drawImage(
|
||||
linkCanvasCache,
|
||||
x - centerX,
|
||||
y - centerY,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
} else {
|
||||
context.drawImage(
|
||||
linkCanvasCache,
|
||||
x - centerX,
|
||||
y - centerY,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
}
|
||||
context.restore();
|
||||
}
|
||||
};
|
||||
|
||||
const isVisibleElement = (
|
||||
element: ExcalidrawElement,
|
||||
canvasWidth: number,
|
||||
|
@ -64,6 +64,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -89,6 +90,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -143,6 +145,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -232,6 +235,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -257,6 +261,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -284,6 +289,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -338,6 +344,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -376,6 +383,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -400,6 +408,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -438,6 +447,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -462,6 +472,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -551,6 +562,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -576,6 +588,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -603,6 +616,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -657,6 +671,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -695,6 +710,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -719,6 +735,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -757,6 +774,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -781,6 +799,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -870,6 +889,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -895,6 +915,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -949,6 +970,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -1036,6 +1058,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -1061,6 +1084,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": true,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1115,6 +1139,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1151,6 +1176,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": true,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1240,6 +1266,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -1265,6 +1292,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1292,6 +1320,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0_copy",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -1346,6 +1375,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1384,6 +1414,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1408,6 +1439,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0_copy",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -1503,6 +1535,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -1530,6 +1563,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1559,6 +1593,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -1613,6 +1648,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1651,6 +1687,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1675,6 +1712,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -1719,6 +1757,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1745,6 +1784,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -1834,6 +1874,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -1859,6 +1900,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 60,
|
||||
"roughness": 2,
|
||||
"seed": 1278240551,
|
||||
@ -1886,6 +1928,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 60,
|
||||
"roughness": 2,
|
||||
"seed": 400692809,
|
||||
@ -1940,6 +1983,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -1978,6 +2022,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2002,6 +2047,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -2040,6 +2086,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2064,6 +2111,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -2102,6 +2150,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2126,6 +2175,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -2164,6 +2214,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2188,6 +2239,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -2226,6 +2278,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2250,6 +2303,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -2288,6 +2342,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2312,6 +2367,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -2350,6 +2406,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2374,6 +2431,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 2,
|
||||
"seed": 400692809,
|
||||
@ -2412,6 +2470,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2436,6 +2495,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 60,
|
||||
"roughness": 2,
|
||||
"seed": 400692809,
|
||||
@ -2474,6 +2534,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 60,
|
||||
"roughness": 2,
|
||||
"seed": 1278240551,
|
||||
@ -2498,6 +2559,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 60,
|
||||
"roughness": 2,
|
||||
"seed": 400692809,
|
||||
@ -2587,6 +2649,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -2612,6 +2675,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -2639,6 +2703,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2693,6 +2758,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2731,6 +2797,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2755,6 +2822,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -2793,6 +2861,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -2817,6 +2886,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -2906,6 +2976,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -2931,6 +3002,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -2958,6 +3030,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -3012,6 +3085,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -3050,6 +3124,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -3074,6 +3149,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -3112,6 +3188,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -3136,6 +3213,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -3229,6 +3307,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -3254,6 +3333,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -3281,6 +3361,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 401146281,
|
||||
@ -3335,6 +3416,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -3373,6 +3455,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -3397,6 +3480,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 401146281,
|
||||
@ -3441,6 +3525,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -3467,6 +3552,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 401146281,
|
||||
@ -3507,6 +3593,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -3531,6 +3618,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 401146281,
|
||||
@ -3626,6 +3714,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -3651,6 +3740,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -3678,6 +3768,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -3732,6 +3823,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -3770,6 +3862,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -3794,6 +3887,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 453191,
|
||||
@ -3891,6 +3985,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -3918,6 +4013,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -3947,6 +4043,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 401146281,
|
||||
@ -4001,6 +4098,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -4039,6 +4137,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -4063,6 +4162,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 401146281,
|
||||
@ -4108,6 +4208,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -4134,6 +4235,7 @@ Object {
|
||||
"height": 10,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 401146281,
|
||||
@ -4221,6 +4323,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -4324,6 +4427,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -4403,6 +4507,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
@ -4428,6 +4533,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -4455,6 +4561,7 @@ Object {
|
||||
"height": 200,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
@ -4482,6 +4589,7 @@ Object {
|
||||
"height": 200,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
@ -4536,6 +4644,7 @@ Object {
|
||||
"height": 20,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 1278240551,
|
||||
|
@ -15,6 +15,7 @@ Object {
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
@ -56,6 +57,7 @@ Object {
|
||||
"height": 50,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
@ -85,6 +87,7 @@ Object {
|
||||
"height": 50,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
@ -115,6 +118,7 @@ Object {
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
@ -156,6 +160,7 @@ Object {
|
||||
"height": 50,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
|
@ -10,6 +10,7 @@ Object {
|
||||
"height": 50,
|
||||
"id": "id0_copy",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 401146281,
|
||||
@ -37,6 +38,7 @@ Object {
|
||||
"height": 50,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
@ -64,6 +66,7 @@ Object {
|
||||
"height": 50,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
@ -96,6 +99,7 @@ Object {
|
||||
"height": 100,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
@ -128,6 +132,7 @@ Object {
|
||||
"height": 300,
|
||||
"id": "id1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 449462985,
|
||||
@ -162,6 +167,7 @@ Object {
|
||||
"id": "id2",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
|
@ -16,6 +16,7 @@ Object {
|
||||
70,
|
||||
110,
|
||||
],
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
@ -65,6 +66,7 @@ Object {
|
||||
70,
|
||||
110,
|
||||
],
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -13,6 +13,7 @@ Object {
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
@ -55,6 +56,7 @@ Object {
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
@ -94,6 +96,7 @@ Object {
|
||||
"height": 50,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
@ -121,6 +124,7 @@ Object {
|
||||
"height": 50,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
@ -148,6 +152,7 @@ Object {
|
||||
"height": 50,
|
||||
"id": "id0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"seed": 337897,
|
||||
|
@ -136,6 +136,7 @@ describe("contextMenu element", () => {
|
||||
"sendToBack",
|
||||
"bringToFront",
|
||||
"duplicateSelection",
|
||||
"link",
|
||||
];
|
||||
|
||||
expect(contextMenu).not.toBeNull();
|
||||
|
@ -13,6 +13,7 @@ Object {
|
||||
"id": "id-arrow01",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
@ -56,6 +57,7 @@ Object {
|
||||
"height": 200,
|
||||
"id": "1",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 10,
|
||||
"roughness": 2,
|
||||
"seed": Any<Number>,
|
||||
@ -87,6 +89,7 @@ Object {
|
||||
"height": 200,
|
||||
"id": "2",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 10,
|
||||
"roughness": 2,
|
||||
"seed": Any<Number>,
|
||||
@ -118,6 +121,7 @@ Object {
|
||||
"height": 200,
|
||||
"id": "3",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 10,
|
||||
"roughness": 2,
|
||||
"seed": Any<Number>,
|
||||
@ -146,6 +150,7 @@ Object {
|
||||
"id": "id-freedraw01",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [],
|
||||
"pressures": Array [],
|
||||
@ -179,6 +184,7 @@ Object {
|
||||
"id": "id-line01",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
@ -221,6 +227,7 @@ Object {
|
||||
"id": "id-draw01",
|
||||
"isDeleted": false,
|
||||
"lastCommittedPoint": null,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"points": Array [
|
||||
Array [
|
||||
@ -264,6 +271,7 @@ Object {
|
||||
"height": 100,
|
||||
"id": "id-text01",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"originalText": "text",
|
||||
"roughness": 1,
|
||||
@ -299,6 +307,7 @@ Object {
|
||||
"height": 100,
|
||||
"id": "id-text01",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"opacity": 100,
|
||||
"originalText": "test",
|
||||
"roughness": 1,
|
||||
|
1
src/tests/fixtures/elementFixture.ts
vendored
1
src/tests/fixtures/elementFixture.ts
vendored
@ -22,6 +22,7 @@ const elementBase: Omit<ExcalidrawElement, "type"> = {
|
||||
isDeleted: false,
|
||||
boundElements: null,
|
||||
updated: 1,
|
||||
link: null,
|
||||
};
|
||||
|
||||
export const rectangleFixture: ExcalidrawElement = {
|
||||
|
@ -60,6 +60,7 @@ Object {
|
||||
"selectionElement": null,
|
||||
"shouldCacheIgnoreZoom": false,
|
||||
"showHelpDialog": false,
|
||||
"showHyperlinkPopup": false,
|
||||
"showStats": false,
|
||||
"startBoundElement": null,
|
||||
"suggestedBindings": Array [],
|
||||
|
@ -74,7 +74,7 @@ exports[`exportToSvg with default arguments 1`] = `
|
||||
exports[`exportToSvg with exportEmbedScene 1`] = `
|
||||
"
|
||||
<!-- svg-source:excalidraw -->
|
||||
<!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1STU9cdTAwMDMhXHUwMDEwvfdXbPDapLtrv+ytWmNMjFx1MDAxZXpoovFAl9mFlFx1MDAwMlx1MDAwNbZcdTAwMWZp+t9cdTAwMDXaLrrx4l1cdTAwMGUk83hvZph5x06SIHtQgCZcdIJ9gTkjXHUwMDFh71DX41vQhknhnvJcdTAwMTBcdTAwMWJZ61wiMKm1atLrcelcdTAwMDRUXHUwMDFhe+ZcdTAwMDOHNVxia1x1MDAxY+PDxUlyXGa3e2HEq7ZcdTAwMGK9eZuWKyZIvinWo5fZ9Ok9SFx1MDAwM2nvOP2s38RcdTAwMDdf+HbUxDtGLHVYlqZcckaBVdS2QCwq7tuMiLFaruBBcql9IzdpOLH0XHUwMDEyXHUwMDE3q0rLWpDIyVx1MDAwNlx1MDAxOC/LyClcdTAwMTnnc3vg51x1MDAwMeCC1lx1MDAxYVCrwuLaYlx1MDAwYm90RrpcdTAwMDFHlStZUVx1MDAwMcb80EiFXHUwMDBiZlx1MDAwZq1f+f7UM1x00/1s56dYq0tcdTAwMWVkfPCtM1x1MDAwMFx1MDAxMlL1s+FgdJeOm5e43yxP2+irXHUwMDE0YddZNlx1MDAxZadpP1x1MDAxZlxyXHUwMDFiXHUwMDA2MzO3alx1MDAxYtKWmFx1MDAxYohz9CN8jDZcdTAwMTA1581jrVxiPoviV6/WI1xmr6UgKOCn7r97/t3zXHUwMDA391x1MDAwMOdMXHUwMDE5uLjH3eGHXGIrNbdO5ChnL6Etg939L9sqw/H64D2/LfBcdTAwMWRcdTAwMWNPndNcdTAwMTfZZ1DTIn0=<!-- payload-end -->
|
||||
<!-- payload-type:application/vnd.excalidraw+json --><!-- payload-version:2 --><!-- payload-start -->eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1STU9cdTAwMDMhXHUwMDEwvfdXbPDapLtrv+ytWmNMjFx1MDAxZXpoovFAl9mFlFx1MDAwMlx1MDAwNbZcdTAwMWZp+t9cdTAwMDXaLrrx5lVcdTAwMGUk83hvZph5x06SIHtQgCZcdIJ9gTkjXHUwMDFh71DX41vQhknhnvJcdTAwMTBcdTAwMWJZ61wiMKm1atLrcelcdTAwMDRUXHUwMDFhe+ZcdTAwMDOHNVxia1x1MDAxY+PDxUlyXGa3e2HEq7ZcdTAwMGK9eZuWKyZIvinWo5fZ9Ok9SFx1MDAwM2nvOP2s38RcdTAwMDdf+HbUxDtGLHVYlqZcckaBVdS2QCwq7tuMiLFaruBBcql9IzdpOLH0XHUwMDEyXHUwMDE3q0rLWpDIyVx1MDAwNlx1MDAxOC/LyClcdTAwMTnnc3vg51x1MDAwMeCC1lx1MDAxYVCrwuLaYlx1MDAwYm90RrpcdTAwMDFHlStZUVx1MDAwMcb80EiFXHUwMDBiZlx1MDAwZq1f+f7UM1x00/1s56dYq0tcdTAwMWVkfPCtM1x1MDAwMFx1MDAxMlL1s+FgdJeOm5e43yxP2+irXHUwMDE0YddZNlx1MDAxZadpP1x1MDAxZlxyXHUwMDFiXHUwMDA2MzO3alx1MDAxYtKWmFx1MDAxYohz9CN8jDZcdTAwMTA1581jrVxiPoviVzlcdTAwMTOrNu9qR8LwWlxuglx1MDAwMn7q/jvq31F/dFx1MDAxNHDOlIGLo9xcdTAwMWR+jbBSc+tcdTAwMTI5ytlfaMtgd//LXHUwMDA2y3C8PvjRb1x1MDAxMHxXx1Pn9Fx1MDAwNbeWWs0ifQ==<!-- payload-end -->
|
||||
<defs>
|
||||
<style>
|
||||
@font-face {
|
||||
|
@ -151,6 +151,7 @@ export type AppState = {
|
||||
};
|
||||
/** imageElement waiting to be placed on canvas */
|
||||
pendingImageElement: NonDeleted<ExcalidrawImageElement> | null;
|
||||
showHyperlinkPopup: false | "info" | "editor";
|
||||
};
|
||||
|
||||
export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
|
||||
|
Loading…
x
Reference in New Issue
Block a user