mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Merge branch 'master' into arnost/scroll-in-read-only-links
This commit is contained in:
commit
72de65e482
147 changed files with 3599 additions and 1322 deletions
2
packages/excalidraw/.gitignore
vendored
2
packages/excalidraw/.gitignore
vendored
|
@ -1,4 +1,2 @@
|
|||
node_modules
|
||||
types
|
||||
bundle.js
|
||||
bundle.css
|
||||
|
|
|
@ -13,8 +13,11 @@ Please add the latest change on the top under the correct section.
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Features
|
||||
|
||||
- Add `onPointerUp` prop [#7638](https://github.com/excalidraw/excalidraw/pull/7638).
|
||||
|
||||
- Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450)
|
||||
- Remove `ExcalidrawEmbeddableElement.validated` attribute. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
|
|
|
@ -40,8 +40,13 @@ const alignSelectedElements = (
|
|||
alignment: Alignment,
|
||||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const elementsMap = arrayToMap(elements);
|
||||
|
||||
const updatedElements = alignElements(selectedElements, alignment);
|
||||
const updatedElements = alignElements(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
alignment,
|
||||
);
|
||||
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
getOriginalContainerHeightFromCache,
|
||||
resetOriginalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "../element/textWysiwyg";
|
||||
} from "../element/containerCache";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isTextBindableContainer,
|
||||
|
@ -45,8 +45,9 @@ export const actionUnbindText = register({
|
|||
},
|
||||
perform: (elements, appState, _, app) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
selectedElements.forEach((element) => {
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
const { width, height, baseline } = measureText(
|
||||
boundTextElement.originalText,
|
||||
|
@ -106,7 +107,10 @@ export const actionBindText = register({
|
|||
if (
|
||||
textElement &&
|
||||
bindingContainer &&
|
||||
getBoundTextElement(bindingContainer) === null
|
||||
getBoundTextElement(
|
||||
bindingContainer,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) === null
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -32,7 +32,11 @@ const distributeSelectedElements = (
|
|||
) => {
|
||||
const selectedElements = app.scene.getSelectedElements(appState);
|
||||
|
||||
const updatedElements = distributeElements(selectedElements, distribution);
|
||||
const updatedElements = distributeElements(
|
||||
selectedElements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
distribution,
|
||||
);
|
||||
|
||||
const updatedElementsMap = arrayToMap(updatedElements);
|
||||
|
||||
|
|
|
@ -139,7 +139,7 @@ const duplicateElements = (
|
|||
continue;
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, arrayToMap(elements));
|
||||
const isElementAFrameLike = isFrameLikeElement(element);
|
||||
|
||||
if (idsOfElementsToDuplicate.get(element.id)) {
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { register } from "./register";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement, NonDeleted } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
NonDeleted,
|
||||
NonDeletedElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import { resizeMultipleElements } from "../element/resizeElements";
|
||||
import { AppState, PointerDownState } from "../types";
|
||||
import { AppState } from "../types";
|
||||
import { arrayToMap } from "../utils";
|
||||
import { CODES, KEYS } from "../keys";
|
||||
import { getCommonBoundingBox } from "../element/bounds";
|
||||
|
@ -20,7 +25,12 @@ export const actionFlipHorizontal = register({
|
|||
perform: (elements, appState, _, app) => {
|
||||
return {
|
||||
elements: updateFrameMembershipOfSelectedElements(
|
||||
flipSelectedElements(elements, appState, "horizontal"),
|
||||
flipSelectedElements(
|
||||
elements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
appState,
|
||||
"horizontal",
|
||||
),
|
||||
appState,
|
||||
app,
|
||||
),
|
||||
|
@ -38,7 +48,12 @@ export const actionFlipVertical = register({
|
|||
perform: (elements, appState, _, app) => {
|
||||
return {
|
||||
elements: updateFrameMembershipOfSelectedElements(
|
||||
flipSelectedElements(elements, appState, "vertical"),
|
||||
flipSelectedElements(
|
||||
elements,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
appState,
|
||||
"vertical",
|
||||
),
|
||||
appState,
|
||||
app,
|
||||
),
|
||||
|
@ -53,6 +68,7 @@ export const actionFlipVertical = register({
|
|||
|
||||
const flipSelectedElements = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
|
||||
appState: Readonly<AppState>,
|
||||
flipDirection: "horizontal" | "vertical",
|
||||
) => {
|
||||
|
@ -67,6 +83,7 @@ const flipSelectedElements = (
|
|||
|
||||
const updatedElements = flipElements(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
appState,
|
||||
flipDirection,
|
||||
);
|
||||
|
@ -79,15 +96,17 @@ const flipSelectedElements = (
|
|||
};
|
||||
|
||||
const flipElements = (
|
||||
elements: NonDeleted<ExcalidrawElement>[],
|
||||
selectedElements: NonDeleted<ExcalidrawElement>[],
|
||||
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap,
|
||||
appState: AppState,
|
||||
flipDirection: "horizontal" | "vertical",
|
||||
): ExcalidrawElement[] => {
|
||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(elements);
|
||||
const { minX, minY, maxX, maxY } = getCommonBoundingBox(selectedElements);
|
||||
|
||||
resizeMultipleElements(
|
||||
{ originalElements: arrayToMap(elements) } as PointerDownState,
|
||||
elements,
|
||||
elementsMap,
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
"nw",
|
||||
true,
|
||||
flipDirection === "horizontal" ? maxX : minX,
|
||||
|
@ -96,7 +115,7 @@ const flipElements = (
|
|||
|
||||
(isBindingEnabled(appState)
|
||||
? bindOrUnbindSelectedElements
|
||||
: unbindLinearElements)(elements);
|
||||
: unbindLinearElements)(selectedElements);
|
||||
|
||||
return elements;
|
||||
return selectedElements;
|
||||
};
|
||||
|
|
|
@ -63,11 +63,7 @@ export const actionRemoveAllElementsFromFrame = register({
|
|||
|
||||
if (isFrameLikeElement(selectedElement)) {
|
||||
return {
|
||||
elements: removeAllElementsFromFrame(
|
||||
elements,
|
||||
selectedElement,
|
||||
appState,
|
||||
),
|
||||
elements: removeAllElementsFromFrame(elements, selectedElement),
|
||||
appState: {
|
||||
...appState,
|
||||
selectedElementIds: {
|
||||
|
|
|
@ -105,10 +105,9 @@ export const actionGroup = register({
|
|||
const frameElementsMap = groupByFrameLikes(selectedElements);
|
||||
|
||||
frameElementsMap.forEach((elementsInFrame, frameId) => {
|
||||
nextElements = removeElementsFromFrame(
|
||||
nextElements,
|
||||
removeElementsFromFrame(
|
||||
elementsInFrame,
|
||||
appState,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -229,7 +228,7 @@ export const actionUngroup = register({
|
|||
nextElements,
|
||||
getElementsInResizingFrame(nextElements, frame, appState),
|
||||
frame,
|
||||
appState,
|
||||
app,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -57,7 +57,9 @@ export const actionGoToCollaborator = register({
|
|||
isBeingFollowed={isBeingFollowed}
|
||||
isCurrentUser={collaborator.isCurrentUser === true}
|
||||
/>
|
||||
{collaborator.username}
|
||||
<div className="UserList__collaborator-name">
|
||||
{collaborator.username}
|
||||
</div>
|
||||
<div
|
||||
className="UserList__collaborator-follow-status-icon"
|
||||
style={{ visibility: isBeingFollowed ? "visible" : "hidden" }}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { AppState, Primitive } from "../types";
|
||||
import { AppClassProperties, AppState, Primitive } from "../types";
|
||||
import {
|
||||
DEFAULT_ELEMENT_BACKGROUND_COLOR_PALETTE,
|
||||
DEFAULT_ELEMENT_BACKGROUND_PICKS,
|
||||
|
@ -66,7 +66,6 @@ import {
|
|||
import { mutateElement, newElementWith } from "../element/mutateElement";
|
||||
import {
|
||||
getBoundTextElement,
|
||||
getContainerElement,
|
||||
getDefaultLineHeight,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
|
@ -189,6 +188,7 @@ const offsetElementAfterFontResize = (
|
|||
const changeFontSize = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
app: AppClassProperties,
|
||||
getNewFontSize: (element: ExcalidrawTextElement) => number,
|
||||
fallbackValue?: ExcalidrawTextElement["fontSize"],
|
||||
) => {
|
||||
|
@ -206,7 +206,10 @@ const changeFontSize = (
|
|||
let newElement: ExcalidrawTextElement = newElementWith(oldElement, {
|
||||
fontSize: newFontSize,
|
||||
});
|
||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
);
|
||||
|
||||
newElement = offsetElementAfterFontResize(oldElement, newElement);
|
||||
|
||||
|
@ -600,10 +603,10 @@ export const actionChangeOpacity = register({
|
|||
export const actionChangeFontSize = register({
|
||||
name: "changeFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
return changeFontSize(elements, appState, () => value, value);
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, () => value, value);
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => (
|
||||
<fieldset>
|
||||
<legend>{t("labels.fontSize")}</legend>
|
||||
<ButtonIconSelect
|
||||
|
@ -641,14 +644,21 @@ export const actionChangeFontSize = register({
|
|||
if (isTextElement(element)) {
|
||||
return element.fontSize;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.fontSize;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection
|
||||
? null
|
||||
|
@ -663,8 +673,8 @@ export const actionChangeFontSize = register({
|
|||
export const actionDecreaseFontSize = register({
|
||||
name: "decreaseFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
return changeFontSize(elements, appState, (element) =>
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, (element) =>
|
||||
Math.round(
|
||||
// get previous value before relative increase (doesn't work fully
|
||||
// due to rounding and float precision issues)
|
||||
|
@ -685,8 +695,8 @@ export const actionDecreaseFontSize = register({
|
|||
export const actionIncreaseFontSize = register({
|
||||
name: "increaseFontSize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
return changeFontSize(elements, appState, (element) =>
|
||||
perform: (elements, appState, value, app) => {
|
||||
return changeFontSize(elements, appState, app, (element) =>
|
||||
Math.round(element.fontSize * (1 + FONT_SIZE_RELATIVE_INCREASE_STEP)),
|
||||
);
|
||||
},
|
||||
|
@ -703,7 +713,7 @@ export const actionIncreaseFontSize = register({
|
|||
export const actionChangeFontFamily = register({
|
||||
name: "changeFontFamily",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
perform: (elements, appState, value, app) => {
|
||||
return {
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
|
@ -717,7 +727,10 @@ export const actionChangeFontFamily = register({
|
|||
lineHeight: getDefaultLineHeight(value),
|
||||
},
|
||||
);
|
||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
|
||||
|
@ -732,7 +745,7 @@ export const actionChangeFontFamily = register({
|
|||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||
const options: {
|
||||
value: FontFamilyValues;
|
||||
text: string;
|
||||
|
@ -772,14 +785,21 @@ export const actionChangeFontFamily = register({
|
|||
if (isTextElement(element)) {
|
||||
return element.fontFamily;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.fontFamily;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection
|
||||
? null
|
||||
|
@ -795,7 +815,7 @@ export const actionChangeFontFamily = register({
|
|||
export const actionChangeTextAlign = register({
|
||||
name: "changeTextAlign",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, value) => {
|
||||
perform: (elements, appState, value, app) => {
|
||||
return {
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
|
@ -806,7 +826,10 @@ export const actionChangeTextAlign = register({
|
|||
oldElement,
|
||||
{ textAlign: value },
|
||||
);
|
||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
|
||||
|
@ -821,7 +844,8 @@ export const actionChangeTextAlign = register({
|
|||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||
const elementsMap = app.scene.getNonDeletedElementsMap();
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{t("labels.textAlign")}</legend>
|
||||
|
@ -854,14 +878,18 @@ export const actionChangeTextAlign = register({
|
|||
if (isTextElement(element)) {
|
||||
return element.textAlign;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
elementsMap,
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.textAlign;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(element, elementsMap) !== null,
|
||||
(hasSelection) =>
|
||||
hasSelection ? null : appState.currentItemTextAlign,
|
||||
)}
|
||||
|
@ -875,7 +903,7 @@ export const actionChangeTextAlign = register({
|
|||
export const actionChangeVerticalAlign = register({
|
||||
name: "changeVerticalAlign",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState, value) => {
|
||||
perform: (elements, appState, value, app) => {
|
||||
return {
|
||||
elements: changeProperty(
|
||||
elements,
|
||||
|
@ -887,7 +915,10 @@ export const actionChangeVerticalAlign = register({
|
|||
{ verticalAlign: value },
|
||||
);
|
||||
|
||||
redrawTextBoundingBox(newElement, getContainerElement(oldElement));
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
app.scene.getContainerElement(oldElement),
|
||||
);
|
||||
return newElement;
|
||||
}
|
||||
|
||||
|
@ -901,7 +932,7 @@ export const actionChangeVerticalAlign = register({
|
|||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => {
|
||||
PanelComponent: ({ elements, appState, updateData, app }) => {
|
||||
return (
|
||||
<fieldset>
|
||||
<ButtonIconSelect<VerticalAlign | false>
|
||||
|
@ -933,14 +964,21 @@ export const actionChangeVerticalAlign = register({
|
|||
if (isTextElement(element) && element.containerId) {
|
||||
return element.verticalAlign;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundTextElement) {
|
||||
return boundTextElement.verticalAlign;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
(element) =>
|
||||
isTextElement(element) || getBoundTextElement(element) !== null,
|
||||
isTextElement(element) ||
|
||||
getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
) !== null,
|
||||
(hasSelection) => (hasSelection ? null : VERTICAL_ALIGN.MIDDLE),
|
||||
)}
|
||||
onChange={(value) => updateData(value)}
|
||||
|
|
|
@ -32,12 +32,15 @@ export let copiedStyles: string = "{}";
|
|||
export const actionCopyStyles = register({
|
||||
name: "copyStyles",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState) => {
|
||||
perform: (elements, appState, formData, app) => {
|
||||
const elementsCopied = [];
|
||||
const element = elements.find((el) => appState.selectedElementIds[el.id]);
|
||||
elementsCopied.push(element);
|
||||
if (element && hasBoundTextElement(element)) {
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(
|
||||
element,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
elementsCopied.push(boundTextElement);
|
||||
}
|
||||
if (element) {
|
||||
|
@ -59,7 +62,7 @@ export const actionCopyStyles = register({
|
|||
export const actionPasteStyles = register({
|
||||
name: "pasteStyles",
|
||||
trackEvent: { category: "element" },
|
||||
perform: (elements, appState) => {
|
||||
perform: (elements, appState, formData, app) => {
|
||||
const elementsCopied = JSON.parse(copiedStyles);
|
||||
const pastedElement = elementsCopied[0];
|
||||
const boundTextElement = elementsCopied[1];
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
import { ExcalidrawElement } from "../element/types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { isPromiseLike } from "../utils";
|
||||
|
||||
const trackAction = (
|
||||
action: Action,
|
||||
|
@ -55,7 +56,7 @@ export class ActionManager {
|
|||
app: AppClassProperties,
|
||||
) {
|
||||
this.updater = (actionResult) => {
|
||||
if (actionResult && "then" in actionResult) {
|
||||
if (isPromiseLike(actionResult)) {
|
||||
actionResult.then((actionResult) => {
|
||||
return updater(actionResult);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ExcalidrawElement } from "./element/types";
|
||||
import { ElementsMap, ExcalidrawElement } from "./element/types";
|
||||
import { newElementWith } from "./element/mutateElement";
|
||||
import { BoundingBox, getCommonBoundingBox } from "./element/bounds";
|
||||
import { getMaximumGroups } from "./groups";
|
||||
|
@ -10,10 +10,13 @@ export interface Alignment {
|
|||
|
||||
export const alignElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
alignment: Alignment,
|
||||
): ExcalidrawElement[] => {
|
||||
const groups: ExcalidrawElement[][] = getMaximumGroups(selectedElements);
|
||||
|
||||
const groups: ExcalidrawElement[][] = getMaximumGroups(
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
);
|
||||
const selectionBoundingBox = getCommonBoundingBox(selectedElements);
|
||||
|
||||
return groups.flatMap((group) => {
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<svg width="178" height="162" viewBox="0 0 178 162" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M40.3329 54.3823L38.5547 94.3134L39.7731 111.754L40.1282 118.907L41.0832 123.59L44.3502 131.942L48.9438 137.693L52.5472 143.333L58.5544 147.755L62.5364 150.239L72.3634 154.486L83.15 156.361L91.1212 158.708L101.174 157.525L110.808 156.719L115.983 154.049L124.511 151.377L129.276 148.71L133.701 143.947L139.666 135.877L142.001 128.136L145.746 118.192L145.188 111.065L145.489 94.3675L145.873 75.2546L143.227 59.7779L142.022 47.4695L138.595 46.8345L102.952 45.4703L56.9173 46.7498L46.0719 49.1207L41.9323 50.6825L39.5684 53.4297" fill="#E3E2FE"/>
|
||||
<path d="M41.0014 54.2859C41.0861 64.8796 38.3765 102.581 40.9779 117.876C43.5793 133.17 48.2646 139.346 56.6121 146.047C64.9596 152.746 79.1214 157.662 91.0653 158.078C103.009 158.492 119.347 155.242 128.277 148.543C137.206 141.842 142.112 133.527 144.641 117.874C147.169 102.221 146.061 66.4132 143.446 54.6222C140.83 42.8289 143.97 48.2857 128.948 47.1238C113.925 45.9619 67.9608 46.477 53.3051 47.6483C38.6493 48.8197 43.2053 53.0675 41.0155 54.1518M40.5263 53.9801C40.5404 64.6138 37.9249 103.418 40.5921 118.587C43.257 133.755 48.147 138.325 56.5251 144.991C64.9008 151.655 78.7263 157.935 90.8536 158.577C102.981 159.221 120.212 155.413 129.289 148.844C138.368 142.277 142.872 134.995 145.321 119.168C147.767 103.343 146.805 65.8698 143.977 53.8837C141.148 41.8975 143.615 48.4292 128.348 47.2508C113.081 46.0724 67.14 45.6726 52.3737 46.8157C37.6074 47.9564 41.7776 53.091 39.7524 54.1024" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M64.7935 45.726L66.36 36.6964L65.5979 34.4501L67.3384 30.7668L70.4197 26.5048L74.8157 21.0598L81.9095 16.9131L89.9419 14.4951L95.6127 12.8534L97.7555 13.2133L103.819 15.269L106.552 18.5807L109.967 22.3793L114.45 27.2387L114.904 34.3937L117.153 38.9214L116.031 44.5005L116.765 45.5448L118.863 47.3559L126.164 47.6782L127.744 46.7797L128.76 44.7052L123.882 25.8227L120.641 20.4835L116.351 15.8758L112.809 11.0729L108.617 7.36601L101.352 5.43025L93.263 3.31104L84.2451 5.1433L77.7299 7.86229L76.0011 8.0975L62.1168 19.8532L59.5366 24.5597L55.7733 32.3215L55.0371 39.5282L56.0626 44.8933L56.5636 47.1725L59.6401 46.8644L66.1648 46.0388" fill="#E3E2FE"/>
|
||||
<path d="M65.0037 46.0106C65.1166 43.8231 64.9237 36.8492 66.1421 33.2412C67.3605 29.6307 69.0799 27.2857 72.314 24.3527C75.5481 21.422 81.273 17.6093 85.5444 15.6453C89.8157 13.6813 94.3317 12.1148 97.9421 12.5664C101.555 13.0157 104.544 15.857 107.219 18.3478C109.893 20.8387 112.356 24.0869 113.986 27.5115C115.618 30.9338 116.389 35.8684 117.006 38.8861C117.622 41.9038 115.992 44.2888 117.69 45.6178C119.388 46.949 125.617 48.1721 127.195 46.8667C128.773 45.5613 127.717 41.1888 127.157 37.7877C126.597 34.3866 125.494 30.1247 123.838 26.4601C122.183 22.7956 119.746 18.9029 117.222 15.7982C114.698 12.6934 112.791 9.9086 108.696 7.83642C104.601 5.76189 97.8081 3.42863 92.6547 3.35337C87.5013 3.2781 81.5529 5.74308 77.7708 7.38717C73.991 9.03127 72.8879 10.6166 69.9666 13.218C67.043 15.8217 62.4306 19.6768 60.2384 23.0026C58.0486 26.3284 57.4818 29.252 56.8185 33.1753C56.1529 37.0962 54.6499 44.39 56.2517 46.5327C57.8511 48.6731 64.7756 45.98 66.4267 46.0247M65.9704 45.5096C65.9845 43.348 64.2652 37.5525 65.5423 33.8456C66.8172 30.1364 70.2959 26.4789 73.6264 23.259C76.9546 20.039 81.3177 16.3015 85.5208 14.5281C89.7216 12.7523 95.1079 11.7903 98.8383 12.6111C102.569 13.432 105.283 16.8072 107.903 19.4486C110.526 22.09 113.146 25.3029 114.567 28.4664C115.987 31.6276 116.03 35.4051 116.425 38.4204C116.82 41.4334 115.124 45.1426 116.937 46.5515C118.751 47.9604 125.539 48.2968 127.31 46.8761C129.081 45.4578 127.978 41.4428 127.562 38.0347C127.145 34.6265 126.501 30.1646 124.81 26.4296C123.116 22.6921 120.195 18.7594 117.413 15.6194C114.63 12.4818 112.247 9.39349 108.117 7.58945C103.987 5.78541 97.5776 5.02099 92.6335 4.79519C87.6895 4.56939 82.3503 4.78813 78.4505 6.23466C74.5484 7.68118 72.0882 10.6542 69.228 13.4696C66.3679 16.2851 63.4725 19.7873 61.2898 23.1319C59.1071 26.4789 56.9761 29.4896 56.1293 33.5469C55.285 37.6043 54.577 45.2132 56.2117 47.4759C57.8487 49.7409 64.2675 47.3418 65.9445 47.1301" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M140.37 54.8958C137.884 58.1322 127.704 71.2286 125.185 74.5427M139.697 54.209C137.098 57.5466 127.005 71.7884 124.51 75.3565" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M141.663 63.1765C139.661 66.0413 131.311 77.1501 129.077 79.9726M141.065 62.5908C139.021 65.2792 130.631 76.1364 128.717 78.8625" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M141.888 72.9917C139.475 75.8589 130.268 86.8478 127.966 89.7455M141.02 72.726C138.503 75.6496 129.775 87.2476 127.58 90.3242" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M141.948 82.215C139.815 85.1057 130.308 96.8214 127.961 99.7709M141.459 81.7375C139.298 84.4119 129.816 95.9888 127.479 98.8606" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M141.357 91.7838C138.885 95.2484 128.808 108.535 126.428 111.76M142.474 91.4757C139.917 94.7921 128.38 107.493 125.781 110.883" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M142.568 101.479C140.028 104.403 129.867 115.528 127.195 118.356M141.811 101.018C139.212 104.055 129.477 115.975 126.828 118.97" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M141.023 112.172C138.591 114.775 128.028 125.905 125.422 128.664M140.51 113.465C138.008 116.147 127.36 125.233 124.742 127.582" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M139.004 123.69C136.501 126.275 125.952 137.248 123.287 140.108M138.343 124.817C135.805 127.454 125.487 138.261 122.848 140.75" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M132.192 139.862C129.854 141.624 120.87 148.168 118.574 150.012M131.39 139.496C128.97 141.333 120.524 148.89 118.322 150.621" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M82.6351 92.3124L78.2767 89.0148L78.6718 88.8784L75.6282 79.6865L74.4922 76.0525L75.0379 74.1074L78.6248 69.5444L83.6182 65.186L86.6924 64.0711L93.7768 63.9864L99.9181 63.9276L103.905 64.4215L106.038 66.068L109.333 67.6392L110.251 69.4479L112.438 73.1877L112.702 81.928L111.674 82.93L110.907 85.5573L107.828 89.2336L101.273 92.9193L102.785 120.401L99.5488 125.521L98.0059 127.838L96.1313 129.414L93.17 130.237L92.2198 130.033L90.1358 129.233L88.8328 126.594L87.8378 95.2549L88.9386 93.3215L86.0409 91.294L80.9533 91.1552" fill="white"/>
|
||||
<path d="M82.8214 92.0607C82.0664 91.4327 79.291 90.7201 77.8539 88.2033C76.4167 85.6866 73.5284 80.4438 74.1964 76.9581C74.862 73.4723 78.6959 69.6384 81.8524 67.2887C85.0089 64.939 88.9227 63.1138 93.1353 62.8574C97.3478 62.6034 103.957 63.9888 107.132 65.7575C110.31 67.5263 111.416 70.5651 112.196 73.4747C112.977 76.3842 112.606 80.6626 111.82 83.2122C111.035 85.7642 109.078 87.1661 107.481 88.7749C105.883 90.3837 103.106 91.2751 102.233 92.8651C101.363 94.4551 102.327 95.3254 102.25 98.3125C102.172 101.3 101.76 107.227 101.767 110.788C101.772 114.349 102.487 116.981 102.285 119.676C102.085 122.374 101.52 125.126 100.556 126.965C99.5917 128.805 98.077 130.256 96.5011 130.715C94.9275 131.171 92.4485 130.36 91.1101 129.713C89.7742 129.066 89.0144 128.341 88.4805 126.836C87.9489 125.331 87.9678 123.964 87.9137 120.681C87.8596 117.397 88.1159 111.599 88.1583 107.14C88.203 102.68 89.2779 96.445 88.1724 93.9236C87.0693 91.4022 82.7791 92.4347 81.5325 92.0137M82.0194 91.6068C81.222 90.7624 78.4536 89.7886 77.3623 87.1567C76.2733 84.5247 74.6621 79.3125 75.4783 75.815C76.2921 72.3151 79.1428 68.3166 82.2522 66.1597C85.3617 64.0029 90.1693 63.062 94.1302 62.8739C98.0911 62.6857 102.925 63.0832 106.02 65.0331C109.118 66.9853 111.834 71.5836 112.705 74.5801C113.572 77.5743 111.949 80.7731 111.234 83.0076C110.519 85.2444 109.835 86.3711 108.417 87.9916C107.001 89.6146 103.738 90.9623 102.732 92.7358C101.725 94.5092 102.351 95.6382 102.377 98.6301C102.405 101.62 102.866 106.949 102.894 110.682C102.922 114.417 102.955 118.291 102.544 121.038C102.13 123.783 101.408 125.54 100.42 127.161C99.4318 128.781 98.1005 130.233 96.6163 130.759C95.1322 131.286 92.9353 130.893 91.51 130.322C90.0846 129.753 88.7769 128.889 88.0618 127.335C87.3468 125.78 87.0128 124.317 87.2198 120.998C87.4268 117.68 89.0874 112.046 89.299 107.422C89.5107 102.798 89.8494 95.9322 88.4946 93.2509C87.1398 90.5695 82.4804 91.4845 81.1679 91.3316" stroke="#6965DB" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M28.1943 139.31C26.7936 139.432 25.332 140.402 23.8703 140.523C23.1395 140.766 22.5914 140.16 21.9824 139.735C21.5561 139.553 21.008 138.461 20.8253 138.219C20.5817 136.884 19.9118 134.276 20.0336 133.002C19.7291 131.364 21.5561 129.787 23.0786 129.727C23.2613 129.727 23.8094 129.787 23.8703 129.787C25.7583 130.151 27.5853 131.546 29.5341 131.728C29.595 131.728 29.6559 131.728 29.6559 131.668C30.4476 130.333 30.204 126.937 30.813 125.542C30.813 125.36 31.1784 123.54 31.1784 123.237C31.6048 122.327 32.1529 121.781 33.1273 122.084C33.7972 122.266 34.6498 122.388 34.6498 123.237V128.635C34.8325 129.242 36.1114 128.999 36.5986 128.999C38.7911 128.028 40.8617 127.422 43.3586 127.058C45.6729 127.179 46.7082 129.242 46.5864 131.304C46.6473 132.396 45.4293 133.245 44.6985 133.973C44.4549 134.094 43.4804 134.519 43.115 134.397C42.2624 133.791 41.1662 134.033 40.1309 134.094C40.1309 134.155 40.0091 134.337 40.07 134.397C41.288 135.853 43.5413 136.096 45.0639 137.066C46.1601 138.34 47.4999 138.643 47.1345 140.341C47.0736 141.191 47.1345 142.1 46.221 142.404C45.9774 142.586 44.5767 142.828 44.2722 142.828C43.9677 142.768 43.3586 142.343 43.115 142.04C40.9835 141.13 38.6693 140.402 36.2332 140.159V145.133C35.9896 146.468 35.6851 147.923 34.6498 148.955C34.2844 149.015 33.1273 149.015 32.7619 148.955C32.4574 148.773 31.4221 147.741 31.1784 147.438C30.5694 145.133 30.4476 142.404 29.6559 140.159C29.1687 139.553 28.986 139.25 28.1943 139.31Z" fill="#6965DB"/>
|
||||
<path d="M59.5964 139.31C58.1956 139.432 56.734 140.402 55.2724 140.523C54.5416 140.766 53.9935 140.16 53.3845 139.735C52.9582 139.553 52.41 138.461 52.2273 138.219C51.9837 136.884 51.3138 134.276 51.4356 133.002C51.1311 131.364 52.9582 129.787 54.4807 129.727C54.6634 129.727 55.2115 129.787 55.2724 129.787C57.1603 130.151 58.9874 131.546 60.9362 131.728C60.9971 131.728 61.058 131.728 61.058 131.668C61.8497 130.333 61.6061 126.937 62.2151 125.542C62.2151 125.36 62.5805 123.54 62.5805 123.237C63.0068 122.327 63.5549 121.781 64.5293 122.084C65.1992 122.266 66.0519 122.388 66.0519 123.237V128.635C66.2346 129.242 67.5135 128.999 68.0007 128.999C70.1931 128.028 72.2638 127.422 74.7607 127.058C77.0749 127.179 78.1103 129.242 77.9885 131.304C78.0494 132.396 76.8313 133.245 76.1005 133.973C75.8569 134.094 74.8825 134.519 74.5171 134.397C73.6645 133.791 72.5683 134.033 71.5329 134.094C71.5329 134.155 71.4112 134.337 71.4721 134.397C72.6901 135.853 74.9434 136.096 76.4659 137.066C77.5621 138.34 78.902 138.643 78.5366 140.341C78.4757 141.191 78.5366 142.1 77.623 142.404C77.3794 142.586 75.9787 142.828 75.6742 142.828C75.3697 142.768 74.7607 142.343 74.5171 142.04C72.3856 141.13 70.0713 140.402 67.6353 140.159V145.133C67.3917 146.468 67.0872 147.923 66.0519 148.955C65.6865 149.015 64.5293 149.015 64.1639 148.955C63.8594 148.773 62.8241 147.741 62.5805 147.438C61.9715 145.133 61.8497 142.404 61.058 140.159C60.5708 139.553 60.3881 139.25 59.5964 139.31Z" fill="#6965DB"/>
|
||||
<path d="M90.9984 139.31C89.5977 139.432 88.1361 140.402 86.6745 140.523C85.9436 140.766 85.3955 140.16 84.7865 139.735C84.3602 139.553 83.8121 138.461 83.6294 138.219C83.3858 136.884 82.7159 134.276 82.8377 133.002C82.5332 131.364 84.3602 129.787 85.8827 129.727C86.0654 129.727 86.6136 129.787 86.6745 129.787C88.5624 130.151 90.3894 131.546 92.3382 131.728C92.3991 131.728 92.46 131.728 92.46 131.668C93.2518 130.333 93.0082 126.937 93.6172 125.542C93.6172 125.36 93.9826 123.54 93.9826 123.237C94.4089 122.327 94.957 121.781 95.9314 122.084C96.6013 122.266 97.4539 122.388 97.4539 123.237V128.635C97.6366 129.242 98.9155 128.999 99.4028 128.999C101.595 128.028 103.666 127.422 106.163 127.058C108.477 127.179 109.512 129.242 109.391 131.304C109.451 132.396 108.233 133.245 107.503 133.973C107.259 134.094 106.285 134.519 105.919 134.397C105.067 133.791 103.97 134.033 102.935 134.094C102.935 134.155 102.813 134.337 102.874 134.397C104.092 135.853 106.345 136.096 107.868 137.066C108.964 138.34 110.304 138.643 109.939 140.341C109.878 141.191 109.939 142.1 109.025 142.404C108.782 142.586 107.381 142.828 107.076 142.828C106.772 142.768 106.163 142.343 105.919 142.04C103.788 141.13 101.473 140.402 99.0373 140.159V145.133C98.7937 146.468 98.4892 147.923 97.4539 148.955C97.0885 149.015 95.9314 149.015 95.566 148.955C95.2615 148.773 94.2262 147.741 93.9826 147.438C93.3736 145.133 93.2518 142.404 92.46 140.159C91.9728 139.553 91.7901 139.25 90.9984 139.31Z" fill="#6965DB"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 13 KiB |
|
@ -1,7 +1,10 @@
|
|||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { ActionManager } from "../actions/manager";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { ExcalidrawElement, ExcalidrawElementType } from "../element/types";
|
||||
import {
|
||||
ExcalidrawElementType,
|
||||
NonDeletedElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { useDevice } from "./App";
|
||||
import {
|
||||
|
@ -44,17 +47,14 @@ import { useTunnels } from "../context/tunnels";
|
|||
|
||||
export const SelectedShapeActions = ({
|
||||
appState,
|
||||
elements,
|
||||
elementsMap,
|
||||
renderAction,
|
||||
}: {
|
||||
appState: UIAppState;
|
||||
elements: readonly ExcalidrawElement[];
|
||||
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap;
|
||||
renderAction: ActionManager["renderAction"];
|
||||
}) => {
|
||||
const targetElements = getTargetElements(
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
);
|
||||
const targetElements = getTargetElements(elementsMap, appState);
|
||||
|
||||
let isSingleElementBoundContainer = false;
|
||||
if (
|
||||
|
@ -137,12 +137,12 @@ export const SelectedShapeActions = ({
|
|||
{renderAction("changeFontFamily")}
|
||||
|
||||
{(appState.activeTool.type === "text" ||
|
||||
suppportsHorizontalAlign(targetElements)) &&
|
||||
suppportsHorizontalAlign(targetElements, elementsMap)) &&
|
||||
renderAction("changeTextAlign")}
|
||||
</>
|
||||
)}
|
||||
|
||||
{shouldAllowVerticalAlign(targetElements) &&
|
||||
{shouldAllowVerticalAlign(targetElements, elementsMap) &&
|
||||
renderAction("changeVerticalAlign")}
|
||||
{(canHaveArrowheads(appState.activeTool.type) ||
|
||||
targetElements.some((element) => canHaveArrowheads(element.type))) && (
|
||||
|
|
|
@ -115,7 +115,6 @@ import {
|
|||
newLinearElement,
|
||||
newTextElement,
|
||||
newImageElement,
|
||||
textWysiwyg,
|
||||
transformElements,
|
||||
updateTextElement,
|
||||
redrawTextBoundingBox,
|
||||
|
@ -217,7 +216,6 @@ import {
|
|||
getNormalizedZoom,
|
||||
getSelectedElements,
|
||||
hasBackground,
|
||||
isOverScrollBars,
|
||||
isSomeElementSelected,
|
||||
} from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
|
@ -350,6 +348,8 @@ import {
|
|||
updateFrameMembershipOfSelectedElements,
|
||||
isElementInFrame,
|
||||
getFrameLikeTitle,
|
||||
getElementsOverlappingFrame,
|
||||
filterElementsEligibleAsFrameChildren,
|
||||
} from "../frame";
|
||||
import {
|
||||
excludeElementsInFramesFromSelection,
|
||||
|
@ -402,7 +402,7 @@ import {
|
|||
import { Emitter } from "../emitter";
|
||||
import { ElementCanvasButtons } from "../element/ElementCanvasButtons";
|
||||
import { MagicCacheData, diagramToHTML } from "../data/magic";
|
||||
import { elementsOverlappingBBox, exportToBlob } from "../../utils/export";
|
||||
import { exportToBlob } from "../../utils/export";
|
||||
import { COLOR_PALETTE } from "../colors";
|
||||
import { ElementCanvasButton } from "./MagicButton";
|
||||
import { MagicIcon, copyIcon, fullscreenIcon } from "./icons";
|
||||
|
@ -414,6 +414,8 @@ import { AnimatedTrail } from "../animated-trail";
|
|||
import { LaserTrails } from "../laser-trails";
|
||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
||||
import { getRenderOpacity } from "../renderer/renderElement";
|
||||
import { textWysiwyg } from "../element/textWysiwyg";
|
||||
import { isOverScrollBars } from "../scene/scrollbars";
|
||||
|
||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||
|
@ -1311,10 +1313,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const FRAME_NAME_EDIT_PADDING = 6;
|
||||
|
||||
const reset = () => {
|
||||
if (f.name?.trim() === "") {
|
||||
mutateElement(f, { name: null });
|
||||
}
|
||||
|
||||
mutateElement(f, { name: f.name?.trim() || null });
|
||||
this.setState({ editingFrame: null });
|
||||
};
|
||||
|
||||
|
@ -1337,6 +1336,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
name: e.target.value,
|
||||
});
|
||||
}}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onBlur={() => reset()}
|
||||
onKeyDown={(event) => {
|
||||
// for some inexplicable reason, `onBlur` triggered on ESC
|
||||
|
@ -1429,7 +1429,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const { renderTopRightUI, renderCustomStats } = this.props;
|
||||
|
||||
const versionNonce = this.scene.getVersionNonce();
|
||||
const { canvasElements, visibleElements } =
|
||||
const { elementsMap, visibleElements } =
|
||||
this.renderer.getRenderableElements({
|
||||
versionNonce,
|
||||
zoom: this.state.zoom,
|
||||
|
@ -1443,6 +1443,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
pendingImageElementId: this.state.pendingImageElementId,
|
||||
});
|
||||
|
||||
const allElementsMap = this.scene.getNonDeletedElementsMap();
|
||||
|
||||
const shouldBlockPointerEvents =
|
||||
!(
|
||||
this.state.editingElement && isLinearElement(this.state.editingElement)
|
||||
|
@ -1639,7 +1641,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
<StaticCanvas
|
||||
canvas={this.canvas}
|
||||
rc={this.rc}
|
||||
elements={canvasElements}
|
||||
elementsMap={elementsMap}
|
||||
allElementsMap={allElementsMap}
|
||||
visibleElements={visibleElements}
|
||||
versionNonce={versionNonce}
|
||||
selectionNonce={
|
||||
|
@ -1660,7 +1663,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
<InteractiveCanvas
|
||||
containerRef={this.excalidrawContainerRef}
|
||||
canvas={this.interactiveCanvas}
|
||||
elements={canvasElements}
|
||||
elementsMap={elementsMap}
|
||||
visibleElements={visibleElements}
|
||||
selectedElements={selectedElements}
|
||||
versionNonce={versionNonce}
|
||||
|
@ -1817,11 +1820,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
const magicFrameChildren = elementsOverlappingBBox({
|
||||
elements: this.scene.getNonDeletedElements(),
|
||||
bounds: magicFrame,
|
||||
type: "overlap",
|
||||
}).filter((el) => !isMagicFrameElement(el));
|
||||
const magicFrameChildren = getElementsOverlappingFrame(
|
||||
this.scene.getNonDeletedElements(),
|
||||
magicFrame,
|
||||
).filter((el) => !isMagicFrameElement(el));
|
||||
|
||||
if (!magicFrameChildren.length) {
|
||||
if (source === "button") {
|
||||
|
@ -2818,7 +2820,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
private renderInteractiveSceneCallback = ({
|
||||
atLeastOneVisibleElement,
|
||||
scrollBars,
|
||||
elements,
|
||||
elementsMap,
|
||||
}: RenderInteractiveSceneCallback) => {
|
||||
if (scrollBars) {
|
||||
currentScrollBars = scrollBars;
|
||||
|
@ -2827,7 +2829,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
// hide when editing text
|
||||
isTextElement(this.state.editingElement)
|
||||
? false
|
||||
: !atLeastOneVisibleElement && elements.length > 0;
|
||||
: !atLeastOneVisibleElement && elementsMap.size > 0;
|
||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||
this.setState({ scrolledOutside });
|
||||
}
|
||||
|
@ -3138,16 +3140,29 @@ class App extends React.Component<AppProps, AppState> {
|
|||
},
|
||||
);
|
||||
|
||||
const nextElements = [
|
||||
const allElements = [
|
||||
...this.scene.getElementsIncludingDeleted(),
|
||||
...newElements,
|
||||
];
|
||||
|
||||
this.scene.replaceAllElements(nextElements);
|
||||
const topLayerFrame = this.getTopLayerFrameAtSceneCoords({ x, y });
|
||||
|
||||
if (topLayerFrame) {
|
||||
const eligibleElements = filterElementsEligibleAsFrameChildren(
|
||||
newElements,
|
||||
topLayerFrame,
|
||||
);
|
||||
addElementsToFrame(allElements, eligibleElements, topLayerFrame);
|
||||
}
|
||||
|
||||
this.scene.replaceAllElements(allElements);
|
||||
|
||||
newElements.forEach((newElement) => {
|
||||
if (isTextElement(newElement) && isBoundToContainer(newElement)) {
|
||||
const container = getContainerElement(newElement);
|
||||
const container = getContainerElement(
|
||||
newElement,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
redrawTextBoundingBox(newElement, container);
|
||||
}
|
||||
});
|
||||
|
@ -3978,7 +3993,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (!isTextElement(selectedElement)) {
|
||||
container = selectedElement as ExcalidrawTextContainer;
|
||||
}
|
||||
const midPoint = getContainerCenter(selectedElement, this.state);
|
||||
const midPoint = getContainerCenter(
|
||||
selectedElement,
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const sceneX = midPoint.x;
|
||||
const sceneY = midPoint.y;
|
||||
this.startTextEditing({
|
||||
|
@ -4295,11 +4314,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.scene.replaceAllElements([
|
||||
...this.scene.getElementsIncludingDeleted().map((_element) => {
|
||||
if (_element.id === element.id && isTextElement(_element)) {
|
||||
return updateTextElement(_element, {
|
||||
text,
|
||||
isDeleted,
|
||||
originalText,
|
||||
});
|
||||
return updateTextElement(
|
||||
_element,
|
||||
getContainerElement(
|
||||
_element,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
),
|
||||
{
|
||||
text,
|
||||
isDeleted,
|
||||
originalText,
|
||||
},
|
||||
);
|
||||
}
|
||||
return _element;
|
||||
}),
|
||||
|
@ -4435,6 +4461,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.frameNameBoundsCache,
|
||||
x,
|
||||
y,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
? allHitElements[allHitElements.length - 2]
|
||||
: elementWithHighestZIndex;
|
||||
|
@ -4464,7 +4491,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||
);
|
||||
|
||||
return getElementsAtPosition(elements, (element) =>
|
||||
hitTest(element, this.state, this.frameNameBoundsCache, x, y),
|
||||
hitTest(
|
||||
element,
|
||||
this.state,
|
||||
this.frameNameBoundsCache,
|
||||
x,
|
||||
y,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
),
|
||||
).filter((element) => {
|
||||
// hitting a frame's element from outside the frame is not considered a hit
|
||||
const containingFrame = getContainingFrame(element);
|
||||
|
@ -4501,7 +4535,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
container,
|
||||
);
|
||||
if (container && parentCenterPosition) {
|
||||
const boundTextElementToContainer = getBoundTextElement(container);
|
||||
const boundTextElementToContainer = getBoundTextElement(
|
||||
container,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (!boundTextElementToContainer) {
|
||||
shouldBindToContainer = true;
|
||||
}
|
||||
|
@ -4514,7 +4551,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
if (isTextElement(selectedElements[0])) {
|
||||
existingTextElement = selectedElements[0];
|
||||
} else if (container) {
|
||||
existingTextElement = getBoundTextElement(selectedElements[0]);
|
||||
existingTextElement = getBoundTextElement(
|
||||
selectedElements[0],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
} else {
|
||||
existingTextElement = this.getTextElementAtPosition(sceneX, sceneY);
|
||||
}
|
||||
|
@ -4723,7 +4763,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
[sceneX, sceneY],
|
||||
)
|
||||
) {
|
||||
const midPoint = getContainerCenter(container, this.state);
|
||||
const midPoint = getContainerCenter(
|
||||
container,
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
sceneX = midPoint.x;
|
||||
sceneY = midPoint.y;
|
||||
|
@ -5359,8 +5403,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (!element) {
|
||||
return;
|
||||
|
@ -5387,6 +5431,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
linearElementEditor,
|
||||
{ x: scenePointerX, y: scenePointerY },
|
||||
this.state,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
|
||||
|
@ -5402,6 +5447,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.frameNameBoundsCache,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
elementsMap,
|
||||
)
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
|
@ -5413,6 +5459,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.frameNameBoundsCache,
|
||||
scenePointerX,
|
||||
scenePointerY,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)
|
||||
) {
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||
|
@ -5882,7 +5929,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
event.preventDefault();
|
||||
|
||||
let nextPastePrevented = false;
|
||||
const isLinux = /Linux/.test(window.navigator.platform);
|
||||
const isLinux =
|
||||
typeof window === undefined
|
||||
? false
|
||||
: /Linux/.test(window.navigator.platform);
|
||||
|
||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRABBING);
|
||||
let { clientX: lastX, clientY: lastY } = event;
|
||||
|
@ -6159,6 +6209,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.history,
|
||||
pointerDownState.origin,
|
||||
linearElementEditor,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (ret.hitElement) {
|
||||
pointerDownState.hit.element = ret.hitElement;
|
||||
|
@ -6573,8 +6624,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
return;
|
||||
}
|
||||
|
||||
if (embedLink.warning) {
|
||||
this.setToast({ message: embedLink.warning, closable: true });
|
||||
if (embedLink.error instanceof URIError) {
|
||||
this.setToast({
|
||||
message: t("toast.unrecognizedLinkFormat"),
|
||||
closable: true,
|
||||
});
|
||||
}
|
||||
|
||||
const element = newEmbeddableElement({
|
||||
|
@ -7094,6 +7148,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
);
|
||||
},
|
||||
linearElementEditor,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (didDrag) {
|
||||
pointerDownState.lastCoords.x = pointerCoords.x;
|
||||
|
@ -7635,6 +7690,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.setState({ pendingImageElementId: null });
|
||||
}
|
||||
|
||||
this.props?.onPointerUp?.(activeTool, pointerDownState);
|
||||
this.onPointerUpEmitter.trigger(
|
||||
this.state.activeTool,
|
||||
pointerDownState,
|
||||
|
@ -7812,13 +7868,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||
groupIds: [],
|
||||
});
|
||||
|
||||
this.scene.replaceAllElements(
|
||||
removeElementsFromFrame(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
[linearElement],
|
||||
this.state,
|
||||
),
|
||||
removeElementsFromFrame(
|
||||
[linearElement],
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
|
||||
this.scene.informMutation();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7828,7 +7883,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.getTopLayerFrameAtSceneCoords(sceneCoords);
|
||||
|
||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||
let nextElements = this.scene.getElementsIncludingDeleted();
|
||||
let nextElements = this.scene.getElementsMapIncludingDeleted();
|
||||
|
||||
const updateGroupIdsAfterEditingGroup = (
|
||||
elements: ExcalidrawElement[],
|
||||
|
@ -7921,7 +7976,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
this.scene.replaceAllElements(
|
||||
addElementsToFrame(
|
||||
this.scene.getElementsIncludingDeleted(),
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
elementsInsideFrame,
|
||||
draggingElement,
|
||||
),
|
||||
|
@ -7969,7 +8024,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.state,
|
||||
),
|
||||
frame,
|
||||
this.state,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -8197,6 +8252,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||
this.frameNameBoundsCache,
|
||||
pointerDownState.origin.x,
|
||||
pointerDownState.origin.y,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
)) ||
|
||||
(!hitElement &&
|
||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements))
|
||||
|
@ -9249,10 +9305,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||
|
||||
if (
|
||||
transformElements(
|
||||
pointerDownState,
|
||||
pointerDownState.originalElements,
|
||||
transformHandleType,
|
||||
selectedElements,
|
||||
pointerDownState.resize.arrowDirection,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
shouldRotateWithDiscreteAngle(event),
|
||||
shouldResizeFromCenter(event),
|
||||
selectedElements.length === 1 && isImageElement(selectedElements[0])
|
||||
|
@ -9262,7 +9318,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||
resizeY,
|
||||
pointerDownState.resize.center.x,
|
||||
pointerDownState.resize.center.y,
|
||||
this.state,
|
||||
)
|
||||
) {
|
||||
this.maybeSuggestBindingForAll(selectedElements);
|
||||
|
@ -9439,7 +9494,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||
let elementCenterX = container.x + container.width / 2;
|
||||
let elementCenterY = container.y + container.height / 2;
|
||||
|
||||
const elementCenter = getContainerCenter(container, appState);
|
||||
const elementCenter = getContainerCenter(
|
||||
container,
|
||||
appState,
|
||||
this.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (elementCenter) {
|
||||
elementCenterX = elementCenter.x;
|
||||
elementCenterY = elementCenter.y;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import clsx from "clsx";
|
||||
import React from "react";
|
||||
import { composeEventHandlers } from "../utils";
|
||||
import "./Button.scss";
|
||||
|
||||
|
|
|
@ -10,11 +10,40 @@
|
|||
background-color: var(--back-color);
|
||||
border-color: var(--border-color);
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--color-surface-lowest);
|
||||
position: absolute;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&[disabled] {
|
||||
pointer-events: none;
|
||||
|
||||
.ExcButton__contents {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
&,
|
||||
&__contents {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
// needed because of .Spinner
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&--color-primary {
|
||||
&.ExcButton--variant-filled {
|
||||
--text-color: var(--color-surface-lowest);
|
||||
--back-color: var(--color-primary);
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--back-color: var(--color-brand-hover);
|
||||
}
|
||||
|
@ -27,9 +56,13 @@
|
|||
&.ExcButton--variant-outlined,
|
||||
&.ExcButton--variant-icon {
|
||||
--text-color: var(--color-primary);
|
||||
--border-color: var(--color-border-outline);
|
||||
--border-color: var(--color-primary);
|
||||
--back-color: transparent;
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--text-color: var(--color-brand-hover);
|
||||
--border-color: var(--color-brand-hover);
|
||||
|
@ -47,6 +80,10 @@
|
|||
--text-color: var(--color-danger-text);
|
||||
--back-color: var(--color-danger-dark);
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--back-color: var(--color-danger-darker);
|
||||
}
|
||||
|
@ -62,6 +99,10 @@
|
|||
--border-color: var(--color-danger);
|
||||
--back-color: transparent;
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--text-color: var(--color-danger-darkest);
|
||||
--border-color: var(--color-danger-darkest);
|
||||
|
@ -79,6 +120,10 @@
|
|||
--text-color: var(--island-bg-color);
|
||||
--back-color: var(--color-gray-50);
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--back-color: var(--color-gray-60);
|
||||
}
|
||||
|
@ -94,6 +139,10 @@
|
|||
--border-color: var(--color-muted);
|
||||
--back-color: var(--island-bg-color);
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--text-color: var(--color-muted-background-darker);
|
||||
--border-color: var(--color-muted-darker);
|
||||
|
@ -111,6 +160,10 @@
|
|||
--text-color: black;
|
||||
--back-color: var(--color-warning-dark);
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--back-color: var(--color-warning-darker);
|
||||
}
|
||||
|
@ -126,6 +179,10 @@
|
|||
--border-color: var(--color-warning-dark);
|
||||
--back-color: var(--input-bg-color);
|
||||
|
||||
.Spinner {
|
||||
--spinner-color: var(--text-color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--text-color: var(--color-warning-darker);
|
||||
--border-color: var(--color-warning-darker);
|
||||
|
@ -138,17 +195,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
border-radius: 0.5rem;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
|
||||
font-family: "Assistant";
|
||||
font-family: var(--font-family);
|
||||
|
||||
user-select: none;
|
||||
|
||||
|
@ -159,9 +210,12 @@
|
|||
font-size: 0.875rem;
|
||||
min-height: 3rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
gap: 0.75rem;
|
||||
|
||||
letter-spacing: 0.4px;
|
||||
|
||||
.ExcButton__contents {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
&--size-medium {
|
||||
|
@ -169,9 +223,12 @@
|
|||
font-size: 0.75rem;
|
||||
min-height: 2.5rem;
|
||||
padding: 0.5rem 1rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
letter-spacing: normal;
|
||||
|
||||
.ExcButton__contents {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
&--variant-icon {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import "./FilledButton.scss";
|
||||
import { AbortError } from "../errors";
|
||||
import Spinner from "./Spinner";
|
||||
import { isPromiseLike } from "../utils";
|
||||
|
||||
export type ButtonVariant = "filled" | "outlined" | "icon";
|
||||
export type ButtonColor = "primary" | "danger" | "warning" | "muted";
|
||||
|
@ -11,7 +14,7 @@ export type FilledButtonProps = {
|
|||
label: string;
|
||||
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
onClick?: (event: React.MouseEvent) => void;
|
||||
|
||||
variant?: ButtonVariant;
|
||||
color?: ButtonColor;
|
||||
|
@ -19,14 +22,14 @@ export type FilledButtonProps = {
|
|||
className?: string;
|
||||
fullWidth?: boolean;
|
||||
|
||||
startIcon?: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
startIcon,
|
||||
icon,
|
||||
onClick,
|
||||
label,
|
||||
variant = "filled",
|
||||
|
@ -37,6 +40,27 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||
},
|
||||
ref,
|
||||
) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const _onClick = async (event: React.MouseEvent) => {
|
||||
const ret = onClick?.(event);
|
||||
|
||||
if (isPromiseLike(ret)) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await ret;
|
||||
} catch (error: any) {
|
||||
if (!(error instanceof AbortError)) {
|
||||
throw error;
|
||||
} else {
|
||||
console.warn(error);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={clsx(
|
||||
|
@ -47,17 +71,21 @@ export const FilledButton = forwardRef<HTMLButtonElement, FilledButtonProps>(
|
|||
{ "ExcButton--fullWidth": fullWidth },
|
||||
className,
|
||||
)}
|
||||
onClick={onClick}
|
||||
onClick={_onClick}
|
||||
type="button"
|
||||
aria-label={label}
|
||||
ref={ref}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{startIcon && (
|
||||
<div className="ExcButton__icon" aria-hidden>
|
||||
{startIcon}
|
||||
</div>
|
||||
)}
|
||||
{variant !== "icon" && (children ?? label)}
|
||||
<div className="ExcButton__contents">
|
||||
{isLoading && <Spinner />}
|
||||
{icon && (
|
||||
<div className="ExcButton__icon" aria-hidden>
|
||||
{icon}
|
||||
</div>
|
||||
)}
|
||||
{variant !== "icon" && (children ?? label)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -16,25 +16,20 @@ const FollowMode = ({
|
|||
onDisconnect,
|
||||
}: FollowModeProps) => {
|
||||
return (
|
||||
<div style={{ position: "relative" }}>
|
||||
<div className="follow-mode" style={{ width, height }}>
|
||||
<div className="follow-mode__badge">
|
||||
<div className="follow-mode__badge__label">
|
||||
Following{" "}
|
||||
<span
|
||||
className="follow-mode__badge__username"
|
||||
title={userToFollow.username}
|
||||
>
|
||||
{userToFollow.username}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onDisconnect}
|
||||
className="follow-mode__disconnect-btn"
|
||||
<div className="follow-mode" style={{ width, height }}>
|
||||
<div className="follow-mode__badge">
|
||||
<div className="follow-mode__badge__label">
|
||||
Following{" "}
|
||||
<span
|
||||
className="follow-mode__badge__username"
|
||||
title={userToFollow.username}
|
||||
>
|
||||
{CloseIcon}
|
||||
</button>
|
||||
{userToFollow.username}
|
||||
</span>
|
||||
</div>
|
||||
<button onClick={onDisconnect} className="follow-mode__disconnect-btn">
|
||||
{CloseIcon}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -12,6 +12,8 @@
|
|||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
user-select: none;
|
||||
|
||||
& h3 {
|
||||
font-family: "Assistant";
|
||||
font-style: normal;
|
||||
|
|
|
@ -271,7 +271,7 @@ const ImageExportModal = ({
|
|||
exportingFrame,
|
||||
})
|
||||
}
|
||||
startIcon={downloadIcon}
|
||||
icon={downloadIcon}
|
||||
>
|
||||
{t("imageExportDialog.button.exportToPng")}
|
||||
</FilledButton>
|
||||
|
@ -283,7 +283,7 @@ const ImageExportModal = ({
|
|||
exportingFrame,
|
||||
})
|
||||
}
|
||||
startIcon={downloadIcon}
|
||||
icon={downloadIcon}
|
||||
>
|
||||
{t("imageExportDialog.button.exportToSvg")}
|
||||
</FilledButton>
|
||||
|
@ -296,7 +296,7 @@ const ImageExportModal = ({
|
|||
exportingFrame,
|
||||
})
|
||||
}
|
||||
startIcon={copyIcon}
|
||||
icon={copyIcon}
|
||||
>
|
||||
{t("imageExportDialog.button.copyPngToClipboard")}
|
||||
</FilledButton>
|
||||
|
|
|
@ -78,7 +78,7 @@ const JSONExportModal = ({
|
|||
onClick={async () => {
|
||||
try {
|
||||
trackEvent("export", "link", `ui (${getFrame()})`);
|
||||
await onExportToBackend(elements, appState, files, canvas);
|
||||
await onExportToBackend(elements, appState, files);
|
||||
onCloseRequest();
|
||||
} catch (error: any) {
|
||||
setAppState({ errorMessage: error.message });
|
||||
|
|
|
@ -226,7 +226,7 @@ const LayerUI = ({
|
|||
>
|
||||
<SelectedShapeActions
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||
renderAction={actionManager.renderAction}
|
||||
/>
|
||||
</Island>
|
||||
|
|
|
@ -183,7 +183,7 @@ export const MobileMenu = ({
|
|||
<Section className="App-mobile-menu" heading="selectedShapeActions">
|
||||
<SelectedShapeActions
|
||||
appState={appState}
|
||||
elements={elements}
|
||||
elementsMap={app.scene.getNonDeletedElementsMap()}
|
||||
renderAction={actionManager.renderAction}
|
||||
/>
|
||||
</Section>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
}
|
||||
|
||||
&__popover {
|
||||
@keyframes RoomDialog__popover__scaleIn {
|
||||
@keyframes ShareableLinkDialog__popover__scaleIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
@ -61,7 +61,7 @@
|
|||
}
|
||||
|
||||
transform-origin: var(--radix-popover-content-transform-origin);
|
||||
animation: RoomDialog__popover__scaleIn 150ms ease-out;
|
||||
animation: ShareableLinkDialog__popover__scaleIn 150ms ease-out;
|
||||
}
|
||||
|
||||
&__linkRow {
|
||||
|
|
|
@ -66,7 +66,7 @@ export const ShareableLinkDialog = ({
|
|||
<FilledButton
|
||||
size="large"
|
||||
label="Copy link"
|
||||
startIcon={copyIcon}
|
||||
icon={copyIcon}
|
||||
onClick={copyRoomLink}
|
||||
/>
|
||||
</Popover.Trigger>
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
|
||||
.default-sidebar-trigger .sidebar-trigger__label {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&.excalidraw--mobile .default-sidebar-trigger .sidebar-trigger__label {
|
||||
|
|
|
@ -13,8 +13,6 @@ import { Button } from "./Button";
|
|||
import { eyeIcon, eyeClosedIcon } from "./icons";
|
||||
|
||||
type TextFieldProps = {
|
||||
value?: string;
|
||||
|
||||
onChange?: (value: string) => void;
|
||||
onClick?: () => void;
|
||||
onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
|
||||
|
@ -26,12 +24,11 @@ type TextFieldProps = {
|
|||
label?: string;
|
||||
placeholder?: string;
|
||||
isRedacted?: boolean;
|
||||
};
|
||||
} & ({ value: string } | { defaultValue: string });
|
||||
|
||||
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
(
|
||||
{
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
fullWidth,
|
||||
|
@ -40,6 +37,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||
selectOnRender,
|
||||
onKeyDown,
|
||||
isRedacted = false,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
|
@ -73,10 +71,17 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
|||
>
|
||||
<input
|
||||
className={clsx({
|
||||
"is-redacted": value && isRedacted && !isTemporarilyUnredacted,
|
||||
"is-redacted":
|
||||
"value" in rest &&
|
||||
rest.value &&
|
||||
isRedacted &&
|
||||
!isTemporarilyUnredacted,
|
||||
})}
|
||||
readOnly={readonly}
|
||||
value={value}
|
||||
value={"value" in rest ? rest.value : undefined}
|
||||
defaultValue={
|
||||
"defaultValue" in rest ? rest.defaultValue : undefined
|
||||
}
|
||||
placeholder={placeholder}
|
||||
ref={innerRef}
|
||||
onChange={(event) => onChange?.(event.target.value)}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useExcalidrawContainer } from "./App";
|
|||
import { AbortError } from "../errors";
|
||||
import Spinner from "./Spinner";
|
||||
import { PointerType } from "../element/types";
|
||||
import { isPromiseLike } from "../utils";
|
||||
|
||||
export type ToolButtonSize = "small" | "medium";
|
||||
|
||||
|
@ -65,7 +66,7 @@ export const ToolButton = React.forwardRef((props: ToolButtonProps, ref) => {
|
|||
const onClick = async (event: React.MouseEvent) => {
|
||||
const ret = "onClick" in props && props.onClick?.(event);
|
||||
|
||||
if (ret && "then" in ret) {
|
||||
if (isPromiseLike(ret)) {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await ret;
|
||||
|
|
|
@ -51,6 +51,12 @@
|
|||
color: var(--color-gray-100);
|
||||
}
|
||||
|
||||
.UserList__collaborator-name {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.UserList__collaborator-follow-status-icon {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
|
|
|
@ -7,6 +7,7 @@ import type { DOMAttributes } from "react";
|
|||
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
||||
import type {
|
||||
InteractiveCanvasRenderConfig,
|
||||
RenderableElementsMap,
|
||||
RenderInteractiveSceneCallback,
|
||||
} from "../../scene/types";
|
||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||
|
@ -15,7 +16,7 @@ import { isRenderThrottlingEnabled } from "../../reactUtils";
|
|||
type InteractiveCanvasProps = {
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
elementsMap: RenderableElementsMap;
|
||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||
versionNonce: number | undefined;
|
||||
|
@ -113,7 +114,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|||
renderInteractiveScene(
|
||||
{
|
||||
canvas: props.canvas,
|
||||
elements: props.elements,
|
||||
elementsMap: props.elementsMap,
|
||||
visibleElements: props.visibleElements,
|
||||
selectedElements: props.selectedElements,
|
||||
scale: window.devicePixelRatio,
|
||||
|
@ -201,10 +202,10 @@ const areEqual = (
|
|||
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
||||
prevProps.versionNonce !== nextProps.versionNonce ||
|
||||
prevProps.scale !== nextProps.scale ||
|
||||
// we need to memoize on element arrays because they may have renewed
|
||||
// we need to memoize on elementsMap because they may have renewed
|
||||
// even if versionNonce didn't change (e.g. we filter elements out based
|
||||
// on appState)
|
||||
prevProps.elements !== nextProps.elements ||
|
||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||
prevProps.selectedElements !== nextProps.selectedElements
|
||||
) {
|
||||
|
|
|
@ -3,14 +3,21 @@ import { RoughCanvas } from "roughjs/bin/canvas";
|
|||
import { renderStaticScene } from "../../renderer/renderScene";
|
||||
import { isShallowEqual } from "../../utils";
|
||||
import type { AppState, StaticCanvasAppState } from "../../types";
|
||||
import type { StaticCanvasRenderConfig } from "../../scene/types";
|
||||
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||
import type {
|
||||
RenderableElementsMap,
|
||||
StaticCanvasRenderConfig,
|
||||
} from "../../scene/types";
|
||||
import type {
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../../element/types";
|
||||
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
||||
|
||||
type StaticCanvasProps = {
|
||||
canvas: HTMLCanvasElement;
|
||||
rc: RoughCanvas;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
elementsMap: RenderableElementsMap;
|
||||
allElementsMap: NonDeletedSceneElementsMap;
|
||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||
versionNonce: number | undefined;
|
||||
selectionNonce: number | undefined;
|
||||
|
@ -63,7 +70,8 @@ const StaticCanvas = (props: StaticCanvasProps) => {
|
|||
canvas,
|
||||
rc: props.rc,
|
||||
scale: props.scale,
|
||||
elements: props.elements,
|
||||
elementsMap: props.elementsMap,
|
||||
allElementsMap: props.allElementsMap,
|
||||
visibleElements: props.visibleElements,
|
||||
appState: props.appState,
|
||||
renderConfig: props.renderConfig,
|
||||
|
@ -106,10 +114,10 @@ const areEqual = (
|
|||
if (
|
||||
prevProps.versionNonce !== nextProps.versionNonce ||
|
||||
prevProps.scale !== nextProps.scale ||
|
||||
// we need to memoize on element arrays because they may have renewed
|
||||
// we need to memoize on elementsMap because they may have renewed
|
||||
// even if versionNonce didn't change (e.g. we filter elements out based
|
||||
// on appState)
|
||||
prevProps.elements !== nextProps.elements ||
|
||||
prevProps.elementsMap !== nextProps.elementsMap ||
|
||||
prevProps.visibleElements !== nextProps.visibleElements
|
||||
) {
|
||||
return false;
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
.excalidraw {
|
||||
.collab-button {
|
||||
--button-bg: var(--color-primary);
|
||||
--button-color: white;
|
||||
--button-color: var(--color-surface-lowest);
|
||||
--button-border: var(--color-primary);
|
||||
|
||||
--button-width: var(--lg-button-size);
|
||||
|
@ -35,12 +35,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.theme--dark {
|
||||
.collab-button {
|
||||
color: var(--color-gray-90);
|
||||
}
|
||||
}
|
||||
|
||||
.CollabButton.is-collaborating {
|
||||
background-color: var(--button-special-active-bg-color);
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { t } from "../../i18n";
|
||||
import { usersIcon } from "../icons";
|
||||
import { share } from "../icons";
|
||||
import { Button } from "../Button";
|
||||
|
||||
import clsx from "clsx";
|
||||
|
@ -17,16 +17,18 @@ const LiveCollaborationTrigger = ({
|
|||
} & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
|
||||
const appState = useUIAppState();
|
||||
|
||||
const showIconOnly = appState.width < 830;
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...rest}
|
||||
className={clsx("collab-button", { active: isCollaborating })}
|
||||
type="button"
|
||||
onSelect={onSelect}
|
||||
style={{ position: "relative" }}
|
||||
style={{ position: "relative", width: showIconOnly ? undefined : "auto" }}
|
||||
title={t("labels.liveCollaboration")}
|
||||
>
|
||||
{usersIcon}
|
||||
{showIconOnly ? share : t("labels.share")}
|
||||
{appState.collaborators.size > 0 && (
|
||||
<div className="CollabButton-collaborators">
|
||||
{appState.collaborators.size}
|
||||
|
|
|
@ -2,7 +2,6 @@ import cssVariables from "./css/variables.module.scss";
|
|||
import { AppProps } from "./types";
|
||||
import { ExcalidrawElement, FontFamilyValues } from "./element/types";
|
||||
import { COLOR_PALETTE } from "./colors";
|
||||
|
||||
export const isDarwin = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
|
||||
export const isWindows = /^Win/.test(navigator.platform);
|
||||
export const isAndroid = /\b(android)\b/i.test(navigator.userAgent);
|
||||
|
@ -143,6 +142,7 @@ export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil;
|
|||
export const DEFAULT_TEXT_ALIGN = "left";
|
||||
export const DEFAULT_VERTICAL_ALIGN = "top";
|
||||
export const DEFAULT_VERSION = "{version}";
|
||||
export const DEFAULT_TRANSFORM_HANDLE_SPACING = 2;
|
||||
|
||||
export const CANVAS_ONLY_ACTIONS = ["selectAll"];
|
||||
|
||||
|
|
|
@ -68,6 +68,7 @@
|
|||
cursor: pointer;
|
||||
background-color: var(--button-bg, var(--island-bg-color));
|
||||
color: var(--button-color, var(--color-on-surface));
|
||||
font-family: var(--ui-font);
|
||||
|
||||
svg {
|
||||
width: var(--button-width, var(--lg-icon-size));
|
||||
|
|
|
@ -76,7 +76,7 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
|||
};
|
||||
|
||||
export const fileSave = (
|
||||
blob: Blob,
|
||||
blob: Blob | Promise<Blob>,
|
||||
opts: {
|
||||
/** supply without the extension */
|
||||
name: string;
|
||||
|
|
|
@ -11,7 +11,6 @@ import {
|
|||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { t } from "../i18n";
|
||||
import { elementsOverlappingBBox } from "../../utils/export";
|
||||
import { isSomeElementSelected, getSelectedElements } from "../scene";
|
||||
import { exportToCanvas, exportToSvg } from "../scene/export";
|
||||
import { ExportType } from "../scene/types";
|
||||
|
@ -20,6 +19,7 @@ import { cloneJSON } from "../utils";
|
|||
import { canvasToBlob } from "./blob";
|
||||
import { fileSave, FileSystemHandle } from "./filesystem";
|
||||
import { serializeAsJSON } from "./json";
|
||||
import { getElementsOverlappingFrame } from "../frame";
|
||||
|
||||
export { loadFromBlob } from "./blob";
|
||||
export { loadFromJSON, saveAsJSON } from "./json";
|
||||
|
@ -56,11 +56,7 @@ export const prepareElementsForExport = (
|
|||
isFrameLikeElement(exportedElements[0])
|
||||
) {
|
||||
exportingFrame = exportedElements[0];
|
||||
exportedElements = elementsOverlappingBBox({
|
||||
elements,
|
||||
bounds: exportingFrame,
|
||||
type: "overlap",
|
||||
});
|
||||
exportedElements = getElementsOverlappingFrame(elements, exportingFrame);
|
||||
} else if (exportedElements.length > 1) {
|
||||
exportedElements = getSelectedElements(
|
||||
elements,
|
||||
|
@ -104,7 +100,7 @@ export const exportCanvas = async (
|
|||
throw new Error(t("alerts.cannotExportEmptyCanvas"));
|
||||
}
|
||||
if (type === "svg" || type === "clipboard-svg") {
|
||||
const tempSvg = await exportToSvg(
|
||||
const svgPromise = exportToSvg(
|
||||
elements,
|
||||
{
|
||||
exportBackground,
|
||||
|
@ -117,9 +113,12 @@ export const exportCanvas = async (
|
|||
files,
|
||||
{ exportingFrame },
|
||||
);
|
||||
|
||||
if (type === "svg") {
|
||||
return await fileSave(
|
||||
new Blob([tempSvg.outerHTML], { type: MIME_TYPES.svg }),
|
||||
return fileSave(
|
||||
svgPromise.then((svg) => {
|
||||
return new Blob([svg.outerHTML], { type: MIME_TYPES.svg });
|
||||
}),
|
||||
{
|
||||
description: "Export to SVG",
|
||||
name,
|
||||
|
@ -128,7 +127,9 @@ export const exportCanvas = async (
|
|||
},
|
||||
);
|
||||
} else if (type === "clipboard-svg") {
|
||||
await copyTextToSystemClipboard(tempSvg.outerHTML);
|
||||
await copyTextToSystemClipboard(
|
||||
await svgPromise.then((svg) => svg.outerHTML),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -141,17 +142,20 @@ export const exportCanvas = async (
|
|||
});
|
||||
|
||||
if (type === "png") {
|
||||
let blob = await canvasToBlob(tempCanvas);
|
||||
let blob = canvasToBlob(tempCanvas);
|
||||
|
||||
if (appState.exportEmbedScene) {
|
||||
blob = await (
|
||||
await import("./image")
|
||||
).encodePngMetadata({
|
||||
blob,
|
||||
metadata: serializeAsJSON(elements, appState, files, "local"),
|
||||
});
|
||||
blob = blob.then((blob) =>
|
||||
import("./image").then(({ encodePngMetadata }) =>
|
||||
encodePngMetadata({
|
||||
blob,
|
||||
metadata: serializeAsJSON(elements, appState, files, "local"),
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return await fileSave(blob, {
|
||||
return fileSave(blob, {
|
||||
description: "Export to PNG",
|
||||
name,
|
||||
// FIXME reintroduce `excalidraw.png` when most people upgrade away
|
||||
|
|
|
@ -40,6 +40,7 @@ import { arrayToMap } from "../utils";
|
|||
import { MarkOptional, Mutable } from "../utility-types";
|
||||
import {
|
||||
detectLineHeight,
|
||||
getContainerElement,
|
||||
getDefaultLineHeight,
|
||||
measureBaseline,
|
||||
} from "../element/textElement";
|
||||
|
@ -179,7 +180,6 @@ const restoreElementWithProperties = <
|
|||
|
||||
const restoreElement = (
|
||||
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||
refreshDimensions = false,
|
||||
): typeof element | null => {
|
||||
switch (element.type) {
|
||||
case "text":
|
||||
|
@ -232,10 +232,6 @@ const restoreElement = (
|
|||
element = bumpVersion(element);
|
||||
}
|
||||
|
||||
if (refreshDimensions) {
|
||||
element = { ...element, ...refreshTextDimensions(element) };
|
||||
}
|
||||
|
||||
return element;
|
||||
case "freedraw": {
|
||||
return restoreElementWithProperties(element, {
|
||||
|
@ -426,10 +422,7 @@ export const restoreElements = (
|
|||
// filtering out selection, which is legacy, no longer kept in elements,
|
||||
// and causing issues if retained
|
||||
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
||||
let migratedElement: ExcalidrawElement | null = restoreElement(
|
||||
element,
|
||||
opts?.refreshDimensions,
|
||||
);
|
||||
let migratedElement: ExcalidrawElement | null = restoreElement(element);
|
||||
if (migratedElement) {
|
||||
const localElement = localElementsMap?.get(element.id);
|
||||
if (localElement && localElement.version > migratedElement.version) {
|
||||
|
@ -462,6 +455,16 @@ export const restoreElements = (
|
|||
} else if (element.boundElements) {
|
||||
repairContainerElement(element, restoredElementsMap);
|
||||
}
|
||||
|
||||
if (opts.refreshDimensions && isTextElement(element)) {
|
||||
Object.assign(
|
||||
element,
|
||||
refreshTextDimensions(
|
||||
element,
|
||||
getContainerElement(element, restoredElementsMap),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return restoredElements;
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
normalizeText,
|
||||
} from "../element/textElement";
|
||||
import {
|
||||
ElementsMap,
|
||||
ExcalidrawArrowElement,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawElement,
|
||||
|
@ -42,7 +43,7 @@ import {
|
|||
VerticalAlign,
|
||||
} from "../element/types";
|
||||
import { MarkOptional } from "../utility-types";
|
||||
import { assertNever, cloneJSON, getFontString } from "../utils";
|
||||
import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils";
|
||||
import { getSizeFromPoints } from "../points";
|
||||
import { randomId } from "../random";
|
||||
|
||||
|
@ -202,6 +203,7 @@ const DEFAULT_DIMENSION = 100;
|
|||
const bindTextToContainer = (
|
||||
container: ExcalidrawElement,
|
||||
textProps: { text: string } & MarkOptional<ElementConstructorOpts, "x" | "y">,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const textElement: ExcalidrawTextElement = newTextElement({
|
||||
x: 0,
|
||||
|
@ -623,6 +625,7 @@ export const convertToExcalidrawElements = (
|
|||
let [container, text] = bindTextToContainer(
|
||||
excalidrawElement,
|
||||
element?.label,
|
||||
arrayToMap(elementStore.getElements()),
|
||||
);
|
||||
elementStore.add(container);
|
||||
elementStore.add(text);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ExcalidrawElement } from "./element/types";
|
||||
import { newElementWith } from "./element/mutateElement";
|
||||
import { getMaximumGroups } from "./groups";
|
||||
import { getCommonBoundingBox } from "./element/bounds";
|
||||
import type { ElementsMap, ExcalidrawElement } from "./element/types";
|
||||
|
||||
export interface Distribution {
|
||||
space: "between";
|
||||
|
@ -10,6 +10,7 @@ export interface Distribution {
|
|||
|
||||
export const distributeElements = (
|
||||
selectedElements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
distribution: Distribution,
|
||||
): ExcalidrawElement[] => {
|
||||
const [start, mid, end, extent] =
|
||||
|
@ -18,7 +19,7 @@ export const distributeElements = (
|
|||
: (["minY", "midY", "maxY", "height"] as const);
|
||||
|
||||
const bounds = getCommonBoundingBox(selectedElements);
|
||||
const groups = getMaximumGroups(selectedElements)
|
||||
const groups = getMaximumGroups(selectedElements, elementsMap)
|
||||
.map((group) => [group, getCommonBoundingBox(group)] as const)
|
||||
.sort((a, b) => a[1][mid] - b[1][mid]);
|
||||
|
||||
|
|
|
@ -120,8 +120,11 @@ export const Hyperlink = ({
|
|||
} else {
|
||||
const { width, height } = element;
|
||||
const embedLink = getEmbedLink(link);
|
||||
if (embedLink?.warning) {
|
||||
setToast({ message: embedLink.warning, closable: true });
|
||||
if (embedLink?.error instanceof URIError) {
|
||||
setToast({
|
||||
message: t("toast.unrecognizedLinkFormat"),
|
||||
closable: true,
|
||||
});
|
||||
}
|
||||
const ar = embedLink
|
||||
? embedLink.intrinsicSize.w / embedLink.intrinsicSize.h
|
||||
|
|
|
@ -321,9 +321,9 @@ export const updateBoundElements = (
|
|||
const simultaneouslyUpdatedElementIds = getSimultaneouslyUpdatedElementIds(
|
||||
simultaneouslyUpdated,
|
||||
);
|
||||
|
||||
const scene = Scene.getScene(changedElement)!;
|
||||
getNonDeletedElements(
|
||||
Scene.getScene(changedElement)!,
|
||||
scene,
|
||||
boundLinearElements.map((el) => el.id),
|
||||
).forEach((element) => {
|
||||
if (!isLinearElement(element)) {
|
||||
|
@ -362,9 +362,12 @@ export const updateBoundElements = (
|
|||
endBinding,
|
||||
changedElement as ExcalidrawBindableElement,
|
||||
);
|
||||
const boundText = getBoundTextElement(element);
|
||||
const boundText = getBoundTextElement(
|
||||
element,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (boundText) {
|
||||
handleBindTextResize(element, false);
|
||||
handleBindTextResize(element, scene.getNonDeletedElementsMap(), false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -5,6 +5,8 @@ import {
|
|||
ExcalidrawFreeDrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMapOrArray,
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
import { distance2d, rotate, rotatePoint } from "../math";
|
||||
import rough from "roughjs/bin/rough";
|
||||
|
@ -73,13 +75,16 @@ export class ElementBounds {
|
|||
) {
|
||||
return cachedBounds.bounds;
|
||||
}
|
||||
|
||||
const bounds = ElementBounds.calculateBounds(element);
|
||||
const scene = Scene.getScene(element);
|
||||
const bounds = ElementBounds.calculateBounds(
|
||||
element,
|
||||
scene?.getNonDeletedElementsMap() || new Map(),
|
||||
);
|
||||
|
||||
// hack to ensure that downstream checks could retrieve element Scene
|
||||
// so as to have correctly calculated bounds
|
||||
// FIXME remove when we get rid of all the id:Scene / element:Scene mapping
|
||||
const shouldCache = Scene.getScene(element);
|
||||
const shouldCache = !!scene;
|
||||
|
||||
if (shouldCache) {
|
||||
ElementBounds.boundsCache.set(element, {
|
||||
|
@ -91,7 +96,10 @@ export class ElementBounds {
|
|||
return bounds;
|
||||
}
|
||||
|
||||
private static calculateBounds(element: ExcalidrawElement): Bounds {
|
||||
private static calculateBounds(
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
): Bounds {
|
||||
let bounds: Bounds;
|
||||
|
||||
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element);
|
||||
|
@ -110,7 +118,7 @@ export class ElementBounds {
|
|||
maxY + element.y,
|
||||
];
|
||||
} else if (isLinearElement(element)) {
|
||||
bounds = getLinearElementRotatedBounds(element, cx, cy);
|
||||
bounds = getLinearElementRotatedBounds(element, cx, cy, elementsMap);
|
||||
} else if (element.type === "diamond") {
|
||||
const [x11, y11] = rotate(cx, y1, cx, cy, element.angle);
|
||||
const [x12, y12] = rotate(cx, y2, cx, cy, element.angle);
|
||||
|
@ -153,15 +161,20 @@ export const getElementAbsoluteCoords = (
|
|||
element: ExcalidrawElement,
|
||||
includeBoundText: boolean = false,
|
||||
): [number, number, number, number, number, number] => {
|
||||
const elementsMap =
|
||||
Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map();
|
||||
if (isFreeDrawElement(element)) {
|
||||
return getFreeDrawElementAbsoluteCoords(element);
|
||||
} else if (isLinearElement(element)) {
|
||||
return LinearElementEditor.getElementAbsoluteCoords(
|
||||
element,
|
||||
elementsMap,
|
||||
includeBoundText,
|
||||
);
|
||||
} else if (isTextElement(element)) {
|
||||
const container = getContainerElement(element);
|
||||
const container = elementsMap
|
||||
? getContainerElement(element, elementsMap)
|
||||
: null;
|
||||
if (isArrowElement(container)) {
|
||||
const coords = LinearElementEditor.getBoundTextElementPosition(
|
||||
container,
|
||||
|
@ -672,7 +685,10 @@ const getLinearElementRotatedBounds = (
|
|||
element: ExcalidrawLinearElement,
|
||||
cx: number,
|
||||
cy: number,
|
||||
elementsMap: ElementsMap,
|
||||
): Bounds => {
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (element.points.length < 2) {
|
||||
const [pointX, pointY] = element.points[0];
|
||||
const [x, y] = rotate(
|
||||
|
@ -684,7 +700,6 @@ const getLinearElementRotatedBounds = (
|
|||
);
|
||||
|
||||
let coords: Bounds = [x, y, x, y];
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
element,
|
||||
|
@ -709,7 +724,6 @@ const getLinearElementRotatedBounds = (
|
|||
rotate(element.x + x, element.y + y, cx, cy, element.angle);
|
||||
const res = getMinMaxXYFromCurvePathOps(ops, transformXY);
|
||||
let coords: Bounds = [res[0], res[1], res[2], res[3]];
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
if (boundTextElement) {
|
||||
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
element,
|
||||
|
@ -729,10 +743,8 @@ const getLinearElementRotatedBounds = (
|
|||
export const getElementBounds = (element: ExcalidrawElement): Bounds => {
|
||||
return ElementBounds.getBounds(element);
|
||||
};
|
||||
export const getCommonBounds = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
): Bounds => {
|
||||
if (!elements.length) {
|
||||
export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => {
|
||||
if ("size" in elements ? !elements.size : !elements.length) {
|
||||
return [0, 0, 0, 0];
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
StrokeRoundness,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawIframeLikeElement,
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
|
||||
import {
|
||||
|
@ -78,6 +79,7 @@ export const hitTest = (
|
|||
frameNameBoundsCache: FrameNameBoundsCache,
|
||||
x: number,
|
||||
y: number,
|
||||
elementsMap: ElementsMap,
|
||||
): boolean => {
|
||||
// How many pixels off the shape boundary we still consider a hit
|
||||
const threshold = 10 / appState.zoom.value;
|
||||
|
@ -95,7 +97,7 @@ export const hitTest = (
|
|||
);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
const isHittingBoundTextElement = hitTest(
|
||||
boundTextElement,
|
||||
|
@ -103,6 +105,7 @@ export const hitTest = (
|
|||
frameNameBoundsCache,
|
||||
x,
|
||||
y,
|
||||
elementsMap,
|
||||
);
|
||||
if (isHittingBoundTextElement) {
|
||||
return true;
|
||||
|
@ -122,15 +125,16 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
|
|||
frameNameBoundsCache: FrameNameBoundsCache,
|
||||
x: number,
|
||||
y: number,
|
||||
elementsMap: ElementsMap,
|
||||
): boolean => {
|
||||
const threshold = 10 / appState.zoom.value;
|
||||
|
||||
// So that bound text element hit is considered within bounding box of container even if its outside actual bounding box of element
|
||||
// eg for linear elements text can be outside the element bounding box
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (
|
||||
boundTextElement &&
|
||||
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y)
|
||||
hitTest(boundTextElement, appState, frameNameBoundsCache, x, y, elementsMap)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
33
packages/excalidraw/element/containerCache.ts
Normal file
33
packages/excalidraw/element/containerCache.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { ExcalidrawTextContainer } from "./types";
|
||||
|
||||
export const originalContainerCache: {
|
||||
[id: ExcalidrawTextContainer["id"]]:
|
||||
| {
|
||||
height: ExcalidrawTextContainer["height"];
|
||||
}
|
||||
| undefined;
|
||||
} = {};
|
||||
|
||||
export const updateOriginalContainerCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
height: ExcalidrawTextContainer["height"],
|
||||
) => {
|
||||
const data =
|
||||
originalContainerCache[id] || (originalContainerCache[id] = { height });
|
||||
data.height = height;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const resetOriginalContainerCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
) => {
|
||||
if (originalContainerCache[id]) {
|
||||
delete originalContainerCache[id];
|
||||
}
|
||||
};
|
||||
|
||||
export const getOriginalContainerHeightFromCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
) => {
|
||||
return originalContainerCache[id]?.height ?? null;
|
||||
};
|
|
@ -57,7 +57,10 @@ export const dragSelectedElements = (
|
|||
// skip arrow labels since we calculate its position during render
|
||||
!isArrowElement(element)
|
||||
) {
|
||||
const textElement = getBoundTextElement(element);
|
||||
const textElement = getBoundTextElement(
|
||||
element,
|
||||
scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
if (textElement) {
|
||||
updateElementCoords(pointerDownState, textElement, adjustedOffset);
|
||||
}
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
import { register } from "../actions/register";
|
||||
import { FONT_FAMILY, VERTICAL_ALIGN } from "../constants";
|
||||
import { t } from "../i18n";
|
||||
import { ExcalidrawProps } from "../types";
|
||||
import { getFontString, updateActiveTool } from "../utils";
|
||||
import { setCursorForShape } from "../cursor";
|
||||
import { newTextElement } from "./newElement";
|
||||
import { getContainerElement, wrapText } from "./textElement";
|
||||
import {
|
||||
isFrameLikeElement,
|
||||
isIframeElement,
|
||||
isIframeLikeElement,
|
||||
} from "./typeChecks";
|
||||
import { wrapText } from "./textElement";
|
||||
import { isIframeElement } from "./typeChecks";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawIframeLikeElement,
|
||||
IframeData,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./types";
|
||||
|
||||
const embeddedLinkCache = new Map<string, IframeData>();
|
||||
|
@ -112,8 +106,8 @@ export const getEmbedLink = (
|
|||
const vimeoLink = link.match(RE_VIMEO);
|
||||
if (vimeoLink?.[1]) {
|
||||
const target = vimeoLink?.[1];
|
||||
const warning = !/^\d+$/.test(target)
|
||||
? t("toast.unrecognizedLinkFormat")
|
||||
const error = !/^\d+$/.test(target)
|
||||
? new URIError("Invalid embed link format")
|
||||
: undefined;
|
||||
type = "video";
|
||||
link = `https://player.vimeo.com/video/${target}?api=1`;
|
||||
|
@ -125,7 +119,7 @@ export const getEmbedLink = (
|
|||
intrinsicSize: aspectRatio,
|
||||
type,
|
||||
});
|
||||
return { link, intrinsicSize: aspectRatio, type, warning };
|
||||
return { link, intrinsicSize: aspectRatio, type, error };
|
||||
}
|
||||
|
||||
const figmaLink = link.match(RE_FIGMA);
|
||||
|
@ -217,21 +211,6 @@ export const getEmbedLink = (
|
|||
return { link, intrinsicSize: aspectRatio, type };
|
||||
};
|
||||
|
||||
export const isIframeLikeOrItsLabel = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
): Boolean => {
|
||||
if (isIframeLikeElement(element)) {
|
||||
return true;
|
||||
}
|
||||
if (element.type === "text") {
|
||||
const container = getContainerElement(element);
|
||||
if (container && isFrameLikeElement(container)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const createPlaceholderEmbeddableLabel = (
|
||||
element: ExcalidrawIframeLikeElement,
|
||||
): ExcalidrawElement => {
|
||||
|
|
|
@ -50,7 +50,6 @@ export {
|
|||
dragNewElement,
|
||||
} from "./dragElements";
|
||||
export { isTextElement, isExcalidrawElement } from "./typeChecks";
|
||||
export { textWysiwyg } from "./textWysiwyg";
|
||||
export { redrawTextBoundingBox } from "./textElement";
|
||||
export {
|
||||
getPerfectElementSize,
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
PointBinding,
|
||||
ExcalidrawBindableElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
import {
|
||||
distance2d,
|
||||
|
@ -193,6 +194,7 @@ export class LinearElementEditor {
|
|||
pointSceneCoords: { x: number; y: number }[],
|
||||
) => void,
|
||||
linearElementEditor: LinearElementEditor,
|
||||
elementsMap: ElementsMap,
|
||||
): boolean {
|
||||
if (!linearElementEditor) {
|
||||
return false;
|
||||
|
@ -272,9 +274,9 @@ export class LinearElementEditor {
|
|||
);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
handleBindTextResize(element, false);
|
||||
handleBindTextResize(element, elementsMap, false);
|
||||
}
|
||||
|
||||
// suggest bindings for first and last point if selected
|
||||
|
@ -404,9 +406,10 @@ export class LinearElementEditor {
|
|||
|
||||
static getEditorMidPoints = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: ElementsMap,
|
||||
appState: InteractiveCanvasAppState,
|
||||
): typeof editorMidPointsCache["points"] => {
|
||||
const boundText = getBoundTextElement(element);
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
|
||||
// Since its not needed outside editor unless 2 pointer lines or bound text
|
||||
if (
|
||||
|
@ -465,6 +468,7 @@ export class LinearElementEditor {
|
|||
linearElementEditor: LinearElementEditor,
|
||||
scenePointer: { x: number; y: number },
|
||||
appState: AppState,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const { elementId } = linearElementEditor;
|
||||
const element = LinearElementEditor.getElement(elementId);
|
||||
|
@ -503,7 +507,7 @@ export class LinearElementEditor {
|
|||
}
|
||||
let index = 0;
|
||||
const midPoints: typeof editorMidPointsCache["points"] =
|
||||
LinearElementEditor.getEditorMidPoints(element, appState);
|
||||
LinearElementEditor.getEditorMidPoints(element, elementsMap, appState);
|
||||
while (index < midPoints.length) {
|
||||
if (midPoints[index] !== null) {
|
||||
const distance = distance2d(
|
||||
|
@ -581,6 +585,7 @@ export class LinearElementEditor {
|
|||
linearElementEditor: LinearElementEditor,
|
||||
appState: AppState,
|
||||
midPoint: Point,
|
||||
elementsMap: ElementsMap,
|
||||
) {
|
||||
const element = LinearElementEditor.getElement(
|
||||
linearElementEditor.elementId,
|
||||
|
@ -588,7 +593,11 @@ export class LinearElementEditor {
|
|||
if (!element) {
|
||||
return -1;
|
||||
}
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(element, appState);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
element,
|
||||
elementsMap,
|
||||
appState,
|
||||
);
|
||||
let index = 0;
|
||||
while (index < midPoints.length) {
|
||||
if (LinearElementEditor.arePointsEqual(midPoint, midPoints[index])) {
|
||||
|
@ -605,6 +614,7 @@ export class LinearElementEditor {
|
|||
history: History,
|
||||
scenePointer: { x: number; y: number },
|
||||
linearElementEditor: LinearElementEditor,
|
||||
elementsMap: ElementsMap,
|
||||
): {
|
||||
didAddPoint: boolean;
|
||||
hitElement: NonDeleted<ExcalidrawElement> | null;
|
||||
|
@ -630,6 +640,7 @@ export class LinearElementEditor {
|
|||
linearElementEditor,
|
||||
scenePointer,
|
||||
appState,
|
||||
elementsMap,
|
||||
);
|
||||
let segmentMidpointIndex = null;
|
||||
if (segmentMidpoint) {
|
||||
|
@ -637,6 +648,7 @@ export class LinearElementEditor {
|
|||
linearElementEditor,
|
||||
appState,
|
||||
segmentMidpoint,
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
if (event.altKey && appState.editingLinearElement) {
|
||||
|
@ -1418,6 +1430,7 @@ export class LinearElementEditor {
|
|||
|
||||
static getElementAbsoluteCoords = (
|
||||
element: ExcalidrawLinearElement,
|
||||
elementsMap: ElementsMap,
|
||||
includeBoundText: boolean = false,
|
||||
): [number, number, number, number, number, number] => {
|
||||
let coords: [number, number, number, number, number, number];
|
||||
|
@ -1462,7 +1475,7 @@ export class LinearElementEditor {
|
|||
if (!includeBoundText) {
|
||||
return coords;
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
coords = LinearElementEditor.getMinMaxXYWithBoundText(
|
||||
element,
|
||||
|
|
|
@ -31,7 +31,6 @@ import { getElementAbsoluteCoords } from ".";
|
|||
import { adjustXYWithRotation } from "../math";
|
||||
import { getResizedElementAbsoluteCoords } from "./bounds";
|
||||
import {
|
||||
getContainerElement,
|
||||
measureText,
|
||||
normalizeText,
|
||||
wrapText,
|
||||
|
@ -333,17 +332,17 @@ const getAdjustedDimensions = (
|
|||
|
||||
export const refreshTextDimensions = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawTextContainer | null,
|
||||
text = textElement.text,
|
||||
) => {
|
||||
if (textElement.isDeleted) {
|
||||
return;
|
||||
}
|
||||
const container = getContainerElement(textElement);
|
||||
if (container) {
|
||||
text = wrapText(
|
||||
text,
|
||||
getFontString(textElement),
|
||||
getBoundTextMaxWidth(container),
|
||||
getBoundTextMaxWidth(container, textElement),
|
||||
);
|
||||
}
|
||||
const dimensions = getAdjustedDimensions(textElement, text);
|
||||
|
@ -352,6 +351,7 @@ export const refreshTextDimensions = (
|
|||
|
||||
export const updateTextElement = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawTextContainer | null,
|
||||
{
|
||||
text,
|
||||
isDeleted,
|
||||
|
@ -365,7 +365,7 @@ export const updateTextElement = (
|
|||
return newElementWith(textElement, {
|
||||
originalText,
|
||||
isDeleted: isDeleted ?? textElement.isDeleted,
|
||||
...refreshTextDimensions(textElement, originalText),
|
||||
...refreshTextDimensions(textElement, container, originalText),
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
ExcalidrawElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawImageElement,
|
||||
ElementsMap,
|
||||
} from "./types";
|
||||
import type { Mutable } from "../utility-types";
|
||||
import {
|
||||
|
@ -41,7 +42,7 @@ import {
|
|||
MaybeTransformHandleType,
|
||||
TransformHandleDirection,
|
||||
} from "./transformHandles";
|
||||
import { AppState, Point, PointerDownState } from "../types";
|
||||
import { Point, PointerDownState } from "../types";
|
||||
import Scene from "../scene/Scene";
|
||||
import {
|
||||
getApproxMinLineWidth,
|
||||
|
@ -68,10 +69,10 @@ export const normalizeAngle = (angle: number): number => {
|
|||
|
||||
// Returns true when transform (resizing/rotation) happened
|
||||
export const transformElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
resizeArrowDirection: "origin" | "end",
|
||||
elementsMap: ElementsMap,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
shouldResizeFromCenter: boolean,
|
||||
shouldMaintainAspectRatio: boolean,
|
||||
|
@ -79,7 +80,6 @@ export const transformElements = (
|
|||
pointerY: number,
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
appState: AppState,
|
||||
) => {
|
||||
if (selectedElements.length === 1) {
|
||||
const [element] = selectedElements;
|
||||
|
@ -89,7 +89,6 @@ export const transformElements = (
|
|||
pointerX,
|
||||
pointerY,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
pointerDownState.originalElements,
|
||||
);
|
||||
updateBoundElements(element);
|
||||
} else if (
|
||||
|
@ -101,6 +100,7 @@ export const transformElements = (
|
|||
) {
|
||||
resizeSingleTextElement(
|
||||
element,
|
||||
elementsMap,
|
||||
transformHandleType,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
|
@ -109,9 +109,10 @@ export const transformElements = (
|
|||
updateBoundElements(element);
|
||||
} else if (transformHandleType) {
|
||||
resizeSingleElement(
|
||||
pointerDownState.originalElements,
|
||||
originalElements,
|
||||
shouldMaintainAspectRatio,
|
||||
element,
|
||||
elementsMap,
|
||||
transformHandleType,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
|
@ -123,8 +124,9 @@ export const transformElements = (
|
|||
} else if (selectedElements.length > 1) {
|
||||
if (transformHandleType === "rotation") {
|
||||
rotateMultipleElements(
|
||||
pointerDownState,
|
||||
originalElements,
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
pointerX,
|
||||
pointerY,
|
||||
shouldRotateWithDiscreteAngle,
|
||||
|
@ -139,8 +141,9 @@ export const transformElements = (
|
|||
transformHandleType === "se"
|
||||
) {
|
||||
resizeMultipleElements(
|
||||
pointerDownState,
|
||||
originalElements,
|
||||
selectedElements,
|
||||
elementsMap,
|
||||
transformHandleType,
|
||||
shouldResizeFromCenter,
|
||||
pointerX,
|
||||
|
@ -157,7 +160,6 @@ const rotateSingleElement = (
|
|||
pointerX: number,
|
||||
pointerY: number,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
originalElements: Map<string, NonDeleted<ExcalidrawElement>>,
|
||||
) => {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2;
|
||||
|
@ -207,6 +209,7 @@ const rescalePointsInElement = (
|
|||
|
||||
const measureFontSizeFromWidth = (
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
elementsMap: ElementsMap,
|
||||
nextWidth: number,
|
||||
nextHeight: number,
|
||||
): { size: number; baseline: number } | null => {
|
||||
|
@ -215,9 +218,9 @@ const measureFontSizeFromWidth = (
|
|||
|
||||
const hasContainer = isBoundToContainer(element);
|
||||
if (hasContainer) {
|
||||
const container = getContainerElement(element);
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (container) {
|
||||
width = getBoundTextMaxWidth(container);
|
||||
width = getBoundTextMaxWidth(container, element);
|
||||
}
|
||||
}
|
||||
const nextFontSize = element.fontSize * (nextWidth / width);
|
||||
|
@ -257,6 +260,7 @@ const getSidesForTransformHandle = (
|
|||
|
||||
const resizeSingleTextElement = (
|
||||
element: NonDeleted<ExcalidrawTextElement>,
|
||||
elementsMap: ElementsMap,
|
||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
|
@ -303,7 +307,12 @@ const resizeSingleTextElement = (
|
|||
if (scale > 0) {
|
||||
const nextWidth = element.width * scale;
|
||||
const nextHeight = element.height * scale;
|
||||
const metrics = measureFontSizeFromWidth(element, nextWidth, nextHeight);
|
||||
const metrics = measureFontSizeFromWidth(
|
||||
element,
|
||||
elementsMap,
|
||||
nextWidth,
|
||||
nextHeight,
|
||||
);
|
||||
if (metrics === null) {
|
||||
return;
|
||||
}
|
||||
|
@ -342,6 +351,7 @@ export const resizeSingleElement = (
|
|||
originalElements: PointerDownState["originalElements"],
|
||||
shouldMaintainAspectRatio: boolean,
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
transformHandleDirection: TransformHandleDirection,
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
|
@ -385,7 +395,7 @@ export const resizeSingleElement = (
|
|||
let scaleY = atStartBoundsHeight / boundsCurrentHeight;
|
||||
|
||||
let boundTextFont: { fontSize?: number; baseline?: number } = {};
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (transformHandleDirection.includes("e")) {
|
||||
scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth;
|
||||
|
@ -448,7 +458,8 @@ export const resizeSingleElement = (
|
|||
|
||||
const nextFont = measureFontSizeFromWidth(
|
||||
boundTextElement,
|
||||
getBoundTextMaxWidth(updatedElement),
|
||||
elementsMap,
|
||||
getBoundTextMaxWidth(updatedElement, boundTextElement),
|
||||
getBoundTextMaxHeight(updatedElement, boundTextElement),
|
||||
);
|
||||
if (nextFont === null) {
|
||||
|
@ -630,6 +641,7 @@ export const resizeSingleElement = (
|
|||
}
|
||||
handleBindTextResize(
|
||||
element,
|
||||
elementsMap,
|
||||
transformHandleDirection,
|
||||
shouldMaintainAspectRatio,
|
||||
);
|
||||
|
@ -637,8 +649,9 @@ export const resizeSingleElement = (
|
|||
};
|
||||
|
||||
export const resizeMultipleElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
transformHandleType: "nw" | "ne" | "sw" | "se",
|
||||
shouldResizeFromCenter: boolean,
|
||||
pointerX: number,
|
||||
|
@ -658,7 +671,7 @@ export const resizeMultipleElements = (
|
|||
}[],
|
||||
element,
|
||||
) => {
|
||||
const origElement = pointerDownState.originalElements.get(element.id);
|
||||
const origElement = originalElements.get(element.id);
|
||||
if (origElement) {
|
||||
acc.push({ orig: origElement, latest: element });
|
||||
}
|
||||
|
@ -679,7 +692,7 @@ export const resizeMultipleElements = (
|
|||
if (!textId) {
|
||||
return acc;
|
||||
}
|
||||
const text = pointerDownState.originalElements.get(textId) ?? null;
|
||||
const text = originalElements.get(textId) ?? null;
|
||||
if (!isBoundToContainer(text)) {
|
||||
return acc;
|
||||
}
|
||||
|
@ -825,7 +838,12 @@ export const resizeMultipleElements = (
|
|||
}
|
||||
|
||||
if (isTextElement(orig)) {
|
||||
const metrics = measureFontSizeFromWidth(orig, width, height);
|
||||
const metrics = measureFontSizeFromWidth(
|
||||
orig,
|
||||
elementsMap,
|
||||
width,
|
||||
height,
|
||||
);
|
||||
if (!metrics) {
|
||||
return;
|
||||
}
|
||||
|
@ -833,7 +851,7 @@ export const resizeMultipleElements = (
|
|||
update.baseline = metrics.baseline;
|
||||
}
|
||||
|
||||
const boundTextElement = pointerDownState.originalElements.get(
|
||||
const boundTextElement = originalElements.get(
|
||||
getBoundTextElementId(orig) ?? "",
|
||||
) as ExcalidrawTextElementWithContainer | undefined;
|
||||
|
||||
|
@ -866,7 +884,7 @@ export const resizeMultipleElements = (
|
|||
newSize: { width, height },
|
||||
});
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement && boundTextFontSize) {
|
||||
mutateElement(
|
||||
boundTextElement,
|
||||
|
@ -876,7 +894,7 @@ export const resizeMultipleElements = (
|
|||
},
|
||||
false,
|
||||
);
|
||||
handleBindTextResize(element, transformHandleType, true);
|
||||
handleBindTextResize(element, elementsMap, transformHandleType, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -884,8 +902,9 @@ export const resizeMultipleElements = (
|
|||
};
|
||||
|
||||
const rotateMultipleElements = (
|
||||
pointerDownState: PointerDownState,
|
||||
originalElements: PointerDownState["originalElements"],
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
pointerX: number,
|
||||
pointerY: number,
|
||||
shouldRotateWithDiscreteAngle: boolean,
|
||||
|
@ -906,8 +925,7 @@ const rotateMultipleElements = (
|
|||
const cx = (x1 + x2) / 2;
|
||||
const cy = (y1 + y2) / 2;
|
||||
const origAngle =
|
||||
pointerDownState.originalElements.get(element.id)?.angle ??
|
||||
element.angle;
|
||||
originalElements.get(element.id)?.angle ?? element.angle;
|
||||
const [rotatedCX, rotatedCY] = rotate(
|
||||
cx,
|
||||
cy,
|
||||
|
@ -926,7 +944,7 @@ const rotateMultipleElements = (
|
|||
);
|
||||
updateBoundElements(element, { simultaneouslyUpdated: elements });
|
||||
|
||||
const boundText = getBoundTextElement(element);
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
if (boundText && !isArrowElement(element)) {
|
||||
mutateElement(
|
||||
boundText,
|
||||
|
|
|
@ -319,17 +319,17 @@ describe("Test measureText", () => {
|
|||
|
||||
it("should return max width when container is rectangle", () => {
|
||||
const container = API.createElement({ type: "rectangle", ...params });
|
||||
expect(getBoundTextMaxWidth(container)).toBe(168);
|
||||
expect(getBoundTextMaxWidth(container, null)).toBe(168);
|
||||
});
|
||||
|
||||
it("should return max width when container is ellipse", () => {
|
||||
const container = API.createElement({ type: "ellipse", ...params });
|
||||
expect(getBoundTextMaxWidth(container)).toBe(116);
|
||||
expect(getBoundTextMaxWidth(container, null)).toBe(116);
|
||||
});
|
||||
|
||||
it("should return max width when container is diamond", () => {
|
||||
const container = API.createElement({ type: "diamond", ...params });
|
||||
expect(getBoundTextMaxWidth(container)).toBe(79);
|
||||
expect(getBoundTextMaxWidth(container, null)).toBe(79);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { getFontString, arrayToMap, isTestEnv, normalizeEOL } from "../utils";
|
||||
import {
|
||||
ElementsMap,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawElementType,
|
||||
ExcalidrawTextContainer,
|
||||
|
@ -22,7 +23,6 @@ import {
|
|||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import { MaybeTransformHandleType } from "./transformHandles";
|
||||
import Scene from "../scene/Scene";
|
||||
import { isTextElement } from ".";
|
||||
import { isBoundToContainer, isArrowElement } from "./typeChecks";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
|
@ -31,11 +31,12 @@ import { isTextBindableContainer } from "./typeChecks";
|
|||
import { getElementAbsoluteCoords } from ".";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import { isHittingElementNotConsideringBoundingBox } from "./collision";
|
||||
|
||||
import { ExtractSetType } from "../utility-types";
|
||||
import {
|
||||
resetOriginalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./textWysiwyg";
|
||||
import { ExtractSetType } from "../utility-types";
|
||||
} from "./containerCache";
|
||||
|
||||
export const normalizeText = (text: string) => {
|
||||
return (
|
||||
|
@ -88,7 +89,7 @@ export const redrawTextBoundingBox = (
|
|||
container,
|
||||
textElement as ExcalidrawTextElementWithContainer,
|
||||
);
|
||||
const maxContainerWidth = getBoundTextMaxWidth(container);
|
||||
const maxContainerWidth = getBoundTextMaxWidth(container, textElement);
|
||||
|
||||
if (!isArrowElement(container) && metrics.height > maxContainerHeight) {
|
||||
const nextHeight = computeContainerDimensionForBoundText(
|
||||
|
@ -161,6 +162,7 @@ export const bindTextToShapeAfterDuplication = (
|
|||
|
||||
export const handleBindTextResize = (
|
||||
container: NonDeletedExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
transformHandleType: MaybeTransformHandleType,
|
||||
shouldMaintainAspectRatio = false,
|
||||
) => {
|
||||
|
@ -169,25 +171,17 @@ export const handleBindTextResize = (
|
|||
return;
|
||||
}
|
||||
resetOriginalContainerCache(container.id);
|
||||
let textElement = Scene.getScene(container)!.getElement(
|
||||
boundTextElementId,
|
||||
) as ExcalidrawTextElement;
|
||||
const textElement = getBoundTextElement(container, elementsMap);
|
||||
if (textElement && textElement.text) {
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
textElement = Scene.getScene(container)!.getElement(
|
||||
boundTextElementId,
|
||||
) as ExcalidrawTextElement;
|
||||
let text = textElement.text;
|
||||
let nextHeight = textElement.height;
|
||||
let nextWidth = textElement.width;
|
||||
const maxWidth = getBoundTextMaxWidth(container);
|
||||
const maxHeight = getBoundTextMaxHeight(
|
||||
container,
|
||||
textElement as ExcalidrawTextElementWithContainer,
|
||||
);
|
||||
const maxWidth = getBoundTextMaxWidth(container, textElement);
|
||||
const maxHeight = getBoundTextMaxHeight(container, textElement);
|
||||
let containerHeight = container.height;
|
||||
let nextBaseLine = textElement.baseline;
|
||||
if (
|
||||
|
@ -242,10 +236,7 @@ export const handleBindTextResize = (
|
|||
if (!isArrowElement(container)) {
|
||||
mutateElement(
|
||||
textElement,
|
||||
computeBoundTextPosition(
|
||||
container,
|
||||
textElement as ExcalidrawTextElementWithContainer,
|
||||
),
|
||||
computeBoundTextPosition(container, textElement),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -263,7 +254,7 @@ export const computeBoundTextPosition = (
|
|||
}
|
||||
const containerCoords = getContainerCoords(container);
|
||||
const maxContainerHeight = getBoundTextMaxHeight(container, boundTextElement);
|
||||
const maxContainerWidth = getBoundTextMaxWidth(container);
|
||||
const maxContainerWidth = getBoundTextMaxWidth(container, boundTextElement);
|
||||
|
||||
let x;
|
||||
let y;
|
||||
|
@ -666,33 +657,32 @@ export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
|||
: null;
|
||||
};
|
||||
|
||||
export const getBoundTextElement = (element: ExcalidrawElement | null) => {
|
||||
export const getBoundTextElement = (
|
||||
element: ExcalidrawElement | null,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
const boundTextElementId = getBoundTextElementId(element);
|
||||
|
||||
if (boundTextElementId) {
|
||||
return (
|
||||
(Scene.getScene(element)?.getElement(
|
||||
boundTextElementId,
|
||||
) as ExcalidrawTextElementWithContainer) || null
|
||||
);
|
||||
return (elementsMap.get(boundTextElementId) ||
|
||||
null) as ExcalidrawTextElementWithContainer | null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const getContainerElement = (
|
||||
element:
|
||||
| (ExcalidrawElement & {
|
||||
containerId: ExcalidrawElement["id"] | null;
|
||||
})
|
||||
| null,
|
||||
) => {
|
||||
element: ExcalidrawTextElement | null,
|
||||
elementsMap: ElementsMap,
|
||||
): ExcalidrawTextContainer | null => {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
if (element.containerId) {
|
||||
return Scene.getScene(element)?.getElement(element.containerId) || null;
|
||||
return (elementsMap.get(element.containerId) ||
|
||||
null) as ExcalidrawTextContainer | null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
@ -700,6 +690,7 @@ export const getContainerElement = (
|
|||
export const getContainerCenter = (
|
||||
container: ExcalidrawElement,
|
||||
appState: AppState,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
if (!isArrowElement(container)) {
|
||||
return {
|
||||
|
@ -719,6 +710,7 @@ export const getContainerCenter = (
|
|||
const index = container.points.length / 2 - 1;
|
||||
let midSegmentMidpoint = LinearElementEditor.getEditorMidPoints(
|
||||
container,
|
||||
elementsMap,
|
||||
appState,
|
||||
)[index];
|
||||
if (!midSegmentMidpoint) {
|
||||
|
@ -752,28 +744,16 @@ export const getContainerCoords = (container: NonDeletedExcalidrawElement) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const getTextElementAngle = (textElement: ExcalidrawTextElement) => {
|
||||
const container = getContainerElement(textElement);
|
||||
export const getTextElementAngle = (
|
||||
textElement: ExcalidrawTextElement,
|
||||
container: ExcalidrawTextContainer | null,
|
||||
) => {
|
||||
if (!container || isArrowElement(container)) {
|
||||
return textElement.angle;
|
||||
}
|
||||
return container.angle;
|
||||
};
|
||||
|
||||
export const getBoundTextElementOffset = (
|
||||
boundTextElement: ExcalidrawTextElement | null,
|
||||
) => {
|
||||
const container = getContainerElement(boundTextElement);
|
||||
if (!container || !boundTextElement) {
|
||||
return 0;
|
||||
}
|
||||
if (isArrowElement(container)) {
|
||||
return BOUND_TEXT_PADDING * 8;
|
||||
}
|
||||
|
||||
return BOUND_TEXT_PADDING;
|
||||
};
|
||||
|
||||
export const getBoundTextElementPosition = (
|
||||
container: ExcalidrawElement,
|
||||
boundTextElement: ExcalidrawTextElementWithContainer,
|
||||
|
@ -788,12 +768,12 @@ export const getBoundTextElementPosition = (
|
|||
|
||||
export const shouldAllowVerticalAlign = (
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
return selectedElements.some((element) => {
|
||||
const hasBoundContainer = isBoundToContainer(element);
|
||||
if (hasBoundContainer) {
|
||||
const container = getContainerElement(element);
|
||||
if (isTextElement(element) && isArrowElement(container)) {
|
||||
if (isBoundToContainer(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (isArrowElement(container)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -804,12 +784,12 @@ export const shouldAllowVerticalAlign = (
|
|||
|
||||
export const suppportsHorizontalAlign = (
|
||||
selectedElements: NonDeletedExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
return selectedElements.some((element) => {
|
||||
const hasBoundContainer = isBoundToContainer(element);
|
||||
if (hasBoundContainer) {
|
||||
const container = getContainerElement(element);
|
||||
if (isTextElement(element) && isArrowElement(container)) {
|
||||
if (isBoundToContainer(element)) {
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (isArrowElement(container)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -890,9 +870,7 @@ export const computeContainerDimensionForBoundText = (
|
|||
|
||||
export const getBoundTextMaxWidth = (
|
||||
container: ExcalidrawElement,
|
||||
boundTextElement: ExcalidrawTextElement | null = getBoundTextElement(
|
||||
container,
|
||||
),
|
||||
boundTextElement: ExcalidrawTextElement | null,
|
||||
) => {
|
||||
const { width } = container;
|
||||
if (isArrowElement(container)) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import {
|
|||
} from "./types";
|
||||
import { API } from "../tests/helpers/api";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { getOriginalContainerHeightFromCache } from "./textWysiwyg";
|
||||
import { getOriginalContainerHeightFromCache } from "./containerCache";
|
||||
import { getTextEditor, updateTextEditor } from "../tests/queries/dom";
|
||||
|
||||
// Unmount ReactDOM from root
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
ExcalidrawLinearElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawTextElement,
|
||||
ExcalidrawTextContainer,
|
||||
} from "./types";
|
||||
import { AppState } from "../types";
|
||||
import { bumpVersion, mutateElement } from "./mutateElement";
|
||||
|
@ -34,6 +33,7 @@ import {
|
|||
computeContainerDimensionForBoundText,
|
||||
detectLineHeight,
|
||||
computeBoundTextPosition,
|
||||
getBoundTextElement,
|
||||
} from "./textElement";
|
||||
import {
|
||||
actionDecreaseFontSize,
|
||||
|
@ -43,6 +43,10 @@ import { actionZoomIn, actionZoomOut } from "../actions/actionCanvas";
|
|||
import App from "../components/App";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { parseClipboard } from "../clipboard";
|
||||
import {
|
||||
originalContainerCache,
|
||||
updateOriginalContainerCache,
|
||||
} from "./containerCache";
|
||||
|
||||
const getTransform = (
|
||||
width: number,
|
||||
|
@ -65,38 +69,6 @@ const getTransform = (
|
|||
return `translate(${translateX}px, ${translateY}px) scale(${zoom.value}) rotate(${degree}deg)`;
|
||||
};
|
||||
|
||||
const originalContainerCache: {
|
||||
[id: ExcalidrawTextContainer["id"]]:
|
||||
| {
|
||||
height: ExcalidrawTextContainer["height"];
|
||||
}
|
||||
| undefined;
|
||||
} = {};
|
||||
|
||||
export const updateOriginalContainerCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
height: ExcalidrawTextContainer["height"],
|
||||
) => {
|
||||
const data =
|
||||
originalContainerCache[id] || (originalContainerCache[id] = { height });
|
||||
data.height = height;
|
||||
return data;
|
||||
};
|
||||
|
||||
export const resetOriginalContainerCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
) => {
|
||||
if (originalContainerCache[id]) {
|
||||
delete originalContainerCache[id];
|
||||
}
|
||||
};
|
||||
|
||||
export const getOriginalContainerHeightFromCache = (
|
||||
id: ExcalidrawTextContainer["id"],
|
||||
) => {
|
||||
return originalContainerCache[id]?.height ?? null;
|
||||
};
|
||||
|
||||
export const textWysiwyg = ({
|
||||
id,
|
||||
onChange,
|
||||
|
@ -153,7 +125,10 @@ export const textWysiwyg = ({
|
|||
if (updatedTextElement && isTextElement(updatedTextElement)) {
|
||||
let coordX = updatedTextElement.x;
|
||||
let coordY = updatedTextElement.y;
|
||||
const container = getContainerElement(updatedTextElement);
|
||||
const container = getContainerElement(
|
||||
updatedTextElement,
|
||||
app.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
let maxWidth = updatedTextElement.width;
|
||||
|
||||
let maxHeight = updatedTextElement.height;
|
||||
|
@ -193,7 +168,8 @@ export const textWysiwyg = ({
|
|||
}
|
||||
}
|
||||
|
||||
maxWidth = getBoundTextMaxWidth(container);
|
||||
maxWidth = getBoundTextMaxWidth(container, updatedTextElement);
|
||||
|
||||
maxHeight = getBoundTextMaxHeight(
|
||||
container,
|
||||
updatedTextElement as ExcalidrawTextElementWithContainer,
|
||||
|
@ -277,7 +253,7 @@ export const textWysiwyg = ({
|
|||
transform: getTransform(
|
||||
textElementWidth,
|
||||
textElementHeight,
|
||||
getTextElementAngle(updatedTextElement),
|
||||
getTextElementAngle(updatedTextElement, container),
|
||||
appState,
|
||||
maxWidth,
|
||||
editorMaxHeight,
|
||||
|
@ -348,17 +324,24 @@ export const textWysiwyg = ({
|
|||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const container = getContainerElement(element);
|
||||
const container = getContainerElement(
|
||||
element,
|
||||
app.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
|
||||
const font = getFontString({
|
||||
fontSize: app.state.currentItemFontSize,
|
||||
fontFamily: app.state.currentItemFontFamily,
|
||||
});
|
||||
if (container) {
|
||||
const boundTextElement = getBoundTextElement(
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
const wrappedText = wrapText(
|
||||
`${editable.value}${data}`,
|
||||
font,
|
||||
getBoundTextMaxWidth(container),
|
||||
getBoundTextMaxWidth(container, boundTextElement),
|
||||
);
|
||||
const width = getTextWidth(wrappedText, font);
|
||||
editable.style.width = `${width}px`;
|
||||
|
@ -528,7 +511,10 @@ export const textWysiwyg = ({
|
|||
return;
|
||||
}
|
||||
let text = editable.value;
|
||||
const container = getContainerElement(updateElement);
|
||||
const container = getContainerElement(
|
||||
updateElement,
|
||||
app.scene.getElementsMapIncludingDeleted(),
|
||||
);
|
||||
|
||||
if (container) {
|
||||
text = updateElement.text;
|
||||
|
|
|
@ -9,7 +9,7 @@ import { rotate } from "../math";
|
|||
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||
import { isTextElement } from ".";
|
||||
import { isFrameLikeElement, isLinearElement } from "./typeChecks";
|
||||
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||
import { DEFAULT_TRANSFORM_HANDLE_SPACING } from "../constants";
|
||||
|
||||
export type TransformHandleDirection =
|
||||
| "n"
|
||||
|
@ -106,7 +106,8 @@ export const getTransformHandlesFromCoords = (
|
|||
const width = x2 - x1;
|
||||
const height = y2 - y1;
|
||||
const dashedLineMargin = margin / zoom.value;
|
||||
const centeringOffset = (size - DEFAULT_SPACING * 2) / (2 * zoom.value);
|
||||
const centeringOffset =
|
||||
(size - DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / (2 * zoom.value);
|
||||
|
||||
const transformHandles: TransformHandles = {
|
||||
nw: omitSides.nw
|
||||
|
@ -263,8 +264,8 @@ export const getTransformHandles = (
|
|||
};
|
||||
}
|
||||
const dashedLineMargin = isLinearElement(element)
|
||||
? DEFAULT_SPACING + 8
|
||||
: DEFAULT_SPACING;
|
||||
? DEFAULT_TRANSFORM_HANDLE_SPACING + 8
|
||||
: DEFAULT_TRANSFORM_HANDLE_SPACING;
|
||||
return getTransformHandlesFromCoords(
|
||||
getElementAbsoluteCoords(element, true),
|
||||
element.angle,
|
||||
|
|
|
@ -214,7 +214,10 @@ export const isBoundToContainer = (
|
|||
};
|
||||
|
||||
export const isUsingAdaptiveRadius = (type: string) =>
|
||||
type === "rectangle" || type === "embeddable" || type === "iframe";
|
||||
type === "rectangle" ||
|
||||
type === "embeddable" ||
|
||||
type === "iframe" ||
|
||||
type === "image";
|
||||
|
||||
export const isUsingProportionalRadius = (type: string) =>
|
||||
type === "line" || type === "arrow" || type === "diamond";
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
THEME,
|
||||
VERTICAL_ALIGN,
|
||||
} from "../constants";
|
||||
import { MarkNonNullable, ValueOf } from "../utility-types";
|
||||
import { MakeBrand, MarkNonNullable, ValueOf } from "../utility-types";
|
||||
import { MagicCacheData } from "../data/magic";
|
||||
|
||||
export type ChartType = "bar" | "line";
|
||||
|
@ -104,7 +104,7 @@ export type ExcalidrawIframeLikeElement =
|
|||
export type IframeData =
|
||||
| {
|
||||
intrinsicSize: { w: number; h: number };
|
||||
warning?: string;
|
||||
error?: Error;
|
||||
} & (
|
||||
| { type: "video" | "generic"; link: string }
|
||||
| { type: "document"; srcdoc: (theme: Theme) => string }
|
||||
|
@ -254,3 +254,41 @@ export type ExcalidrawFreeDrawElement = _ExcalidrawElementBase &
|
|||
export type FileId = string & { _brand: "FileId" };
|
||||
|
||||
export type ExcalidrawElementType = ExcalidrawElement["type"];
|
||||
|
||||
/**
|
||||
* Map of excalidraw elements.
|
||||
* Unspecified whether deleted or non-deleted.
|
||||
* Can be a subset of Scene elements.
|
||||
*/
|
||||
export type ElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement>;
|
||||
|
||||
/**
|
||||
* Map of non-deleted elements.
|
||||
* Can be a subset of Scene elements.
|
||||
*/
|
||||
export type NonDeletedElementsMap = Map<
|
||||
ExcalidrawElement["id"],
|
||||
NonDeletedExcalidrawElement
|
||||
> &
|
||||
MakeBrand<"NonDeletedElementsMap">;
|
||||
|
||||
/**
|
||||
* Map of all excalidraw Scene elements, including deleted.
|
||||
* Not a subset. Use this type when you need access to current Scene elements.
|
||||
*/
|
||||
export type SceneElementsMap = Map<ExcalidrawElement["id"], ExcalidrawElement> &
|
||||
MakeBrand<"SceneElementsMap">;
|
||||
|
||||
/**
|
||||
* Map of all non-deleted Scene elements.
|
||||
* Not a subset. Use this type when you need access to current Scene elements.
|
||||
*/
|
||||
export type NonDeletedSceneElementsMap = Map<
|
||||
ExcalidrawElement["id"],
|
||||
NonDeletedExcalidrawElement
|
||||
> &
|
||||
MakeBrand<"NonDeletedSceneElementsMap">;
|
||||
|
||||
export type ElementsMapOrArray =
|
||||
| readonly ExcalidrawElement[]
|
||||
| Readonly<ElementsMap>;
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
.App {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
|
||||
.comment-avatar {
|
||||
background: #faa2c1;
|
||||
border-radius: 66px 67px 67px 0px;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 4px;
|
||||
margin: 4px;
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.button-wrapper button {
|
||||
z-index: 1;
|
||||
height: 40px;
|
||||
max-width: 200px;
|
||||
margin: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.excalidraw .App-menu_top .buttonList {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.excalidraw-wrapper {
|
||||
height: 800px;
|
||||
margin: 50px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:root[dir="ltr"]
|
||||
.excalidraw
|
||||
.layer-ui__wrapper
|
||||
.zen-mode-transition.App-menu_bottom--transition-left {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.excalidraw .panelColumn {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.export-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 50px;
|
||||
|
||||
&__checkbox {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.excalidraw {
|
||||
--color-primary: #faa2c1;
|
||||
--color-primary-darker: #f783ac;
|
||||
--color-primary-darkest: #e64980;
|
||||
--color-primary-light: #fcc2d7;
|
||||
|
||||
button.custom-element {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.custom-footer,
|
||||
.custom-element {
|
||||
padding: 0.1rem;
|
||||
margin: 0 8px;
|
||||
}
|
||||
.layer-ui__wrapper__footer.App-menu_bottom {
|
||||
align-items: stretch;
|
||||
}
|
||||
// till its merged in OSS
|
||||
.App-toolbar-container .mobile-misc-tools-container {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
|
@ -1,920 +0,0 @@
|
|||
import ExampleSidebar from "./sidebar/ExampleSidebar";
|
||||
|
||||
import type * as TExcalidraw from "../index";
|
||||
|
||||
import "./App.scss";
|
||||
import initialData from "./initialData";
|
||||
import { nanoid } from "nanoid";
|
||||
import { resolvablePromise, ResolvablePromise } from "../utils";
|
||||
import { EVENT, ROUNDNESS } from "../constants";
|
||||
import { distance2d } from "../math";
|
||||
import { fileOpen } from "../data/filesystem";
|
||||
import { loadSceneOrLibraryFromBlob } from "../../utils";
|
||||
import type {
|
||||
AppState,
|
||||
BinaryFileData,
|
||||
ExcalidrawImperativeAPI,
|
||||
ExcalidrawInitialDataState,
|
||||
Gesture,
|
||||
LibraryItems,
|
||||
PointerDownState as ExcalidrawPointerDownState,
|
||||
} from "../types";
|
||||
import type { NonDeletedExcalidrawElement, Theme } from "../element/types";
|
||||
import { ImportedLibraryData } from "../data/types";
|
||||
import CustomFooter from "./CustomFooter";
|
||||
import MobileFooter from "./MobileFooter";
|
||||
import { KEYS } from "../keys";
|
||||
import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ExcalidrawLib: typeof TExcalidraw;
|
||||
}
|
||||
}
|
||||
|
||||
type Comment = {
|
||||
x: number;
|
||||
y: number;
|
||||
value: string;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
type PointerDownState = {
|
||||
x: number;
|
||||
y: number;
|
||||
hitElement: Comment;
|
||||
onMove: any;
|
||||
onUp: any;
|
||||
hitElementOffsets: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
|
||||
const { useEffect, useState, useRef, useCallback } = window.React;
|
||||
|
||||
// This is so that we use the bundled excalidraw.development.js file instead
|
||||
// of the actual source code
|
||||
const {
|
||||
exportToCanvas,
|
||||
exportToSvg,
|
||||
exportToBlob,
|
||||
exportToClipboard,
|
||||
Excalidraw,
|
||||
useHandleLibrary,
|
||||
MIME_TYPES,
|
||||
sceneCoordsToViewportCoords,
|
||||
viewportCoordsToSceneCoords,
|
||||
restoreElements,
|
||||
Sidebar,
|
||||
Footer,
|
||||
WelcomeScreen,
|
||||
MainMenu,
|
||||
LiveCollaborationTrigger,
|
||||
convertToExcalidrawElements,
|
||||
TTDDialog,
|
||||
TTDDialogTrigger,
|
||||
} = window.ExcalidrawLib;
|
||||
|
||||
const COMMENT_ICON_DIMENSION = 32;
|
||||
const COMMENT_INPUT_HEIGHT = 50;
|
||||
const COMMENT_INPUT_WIDTH = 150;
|
||||
|
||||
export interface AppProps {
|
||||
appTitle: string;
|
||||
useCustom: (api: ExcalidrawImperativeAPI | null, customArgs?: any[]) => void;
|
||||
customArgs?: any[];
|
||||
}
|
||||
export default function App({ appTitle, useCustom, customArgs }: AppProps) {
|
||||
const appRef = useRef<any>(null);
|
||||
const [viewModeEnabled, setViewModeEnabled] = useState(false);
|
||||
const [zenModeEnabled, setZenModeEnabled] = useState(false);
|
||||
const [gridModeEnabled, setGridModeEnabled] = useState(false);
|
||||
const [blobUrl, setBlobUrl] = useState<string>("");
|
||||
const [canvasUrl, setCanvasUrl] = useState<string>("");
|
||||
const [exportWithDarkMode, setExportWithDarkMode] = useState(false);
|
||||
const [exportEmbedScene, setExportEmbedScene] = useState(false);
|
||||
const [theme, setTheme] = useState<Theme>("light");
|
||||
const [disableImageTool, setDisableImageTool] = useState(false);
|
||||
const [isCollaborating, setIsCollaborating] = useState(false);
|
||||
const [commentIcons, setCommentIcons] = useState<{ [id: string]: Comment }>(
|
||||
{},
|
||||
);
|
||||
const [comment, setComment] = useState<Comment | null>(null);
|
||||
|
||||
const initialStatePromiseRef = useRef<{
|
||||
promise: ResolvablePromise<ExcalidrawInitialDataState | null>;
|
||||
}>({ promise: null! });
|
||||
if (!initialStatePromiseRef.current.promise) {
|
||||
initialStatePromiseRef.current.promise =
|
||||
resolvablePromise<ExcalidrawInitialDataState | null>();
|
||||
}
|
||||
|
||||
const [excalidrawAPI, setExcalidrawAPI] =
|
||||
useState<ExcalidrawImperativeAPI | null>(null);
|
||||
|
||||
useCustom(excalidrawAPI, customArgs);
|
||||
|
||||
useHandleLibrary({ excalidrawAPI });
|
||||
|
||||
useEffect(() => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
const fetchData = async () => {
|
||||
const res = await fetch("/images/rocket.jpeg");
|
||||
const imageData = await res.blob();
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(imageData);
|
||||
|
||||
reader.onload = function () {
|
||||
const imagesArray: BinaryFileData[] = [
|
||||
{
|
||||
id: "rocket" as BinaryFileData["id"],
|
||||
dataURL: reader.result as BinaryFileData["dataURL"],
|
||||
mimeType: MIME_TYPES.jpg,
|
||||
created: 1644915140367,
|
||||
lastRetrieved: 1644915140367,
|
||||
},
|
||||
];
|
||||
|
||||
//@ts-ignore
|
||||
initialStatePromiseRef.current.promise.resolve({
|
||||
...initialData,
|
||||
elements: convertToExcalidrawElements(initialData.elements),
|
||||
});
|
||||
excalidrawAPI.addFiles(imagesArray);
|
||||
};
|
||||
};
|
||||
fetchData();
|
||||
}, [excalidrawAPI]);
|
||||
|
||||
const renderTopRightUI = (isMobile: boolean) => {
|
||||
return (
|
||||
<>
|
||||
{!isMobile && (
|
||||
<LiveCollaborationTrigger
|
||||
isCollaborating={isCollaborating}
|
||||
onSelect={() => {
|
||||
window.alert("Collab dialog clicked");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={() => alert("This is an empty top right UI")}
|
||||
style={{ height: "2.5rem" }}
|
||||
>
|
||||
Click me
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const loadSceneOrLibrary = async () => {
|
||||
const file = await fileOpen({ description: "Excalidraw or library file" });
|
||||
const contents = await loadSceneOrLibraryFromBlob(file, null, null);
|
||||
if (contents.type === MIME_TYPES.excalidraw) {
|
||||
excalidrawAPI?.updateScene(contents.data as any);
|
||||
} else if (contents.type === MIME_TYPES.excalidrawlib) {
|
||||
excalidrawAPI?.updateLibrary({
|
||||
libraryItems: (contents.data as ImportedLibraryData).libraryItems!,
|
||||
openLibraryMenu: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateScene = () => {
|
||||
const sceneData = {
|
||||
elements: restoreElements(
|
||||
convertToExcalidrawElements([
|
||||
{
|
||||
type: "rectangle",
|
||||
id: "rect-1",
|
||||
fillStyle: "hachure",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
angle: 0,
|
||||
x: 100.50390625,
|
||||
y: 93.67578125,
|
||||
strokeColor: "#c92a2a",
|
||||
width: 186.47265625,
|
||||
height: 141.9765625,
|
||||
seed: 1968410350,
|
||||
roundness: {
|
||||
type: ROUNDNESS.ADAPTIVE_RADIUS,
|
||||
value: 32,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "arrow",
|
||||
x: 300,
|
||||
y: 150,
|
||||
start: { id: "rect-1" },
|
||||
end: { type: "ellipse" },
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
x: 300,
|
||||
y: 100,
|
||||
text: "HELLO WORLD!",
|
||||
},
|
||||
]),
|
||||
null,
|
||||
),
|
||||
appState: {
|
||||
viewBackgroundColor: "#edf2ff",
|
||||
},
|
||||
};
|
||||
excalidrawAPI?.updateScene(sceneData);
|
||||
};
|
||||
|
||||
const onLinkOpen = useCallback(
|
||||
(
|
||||
element: NonDeletedExcalidrawElement,
|
||||
event: CustomEvent<{
|
||||
nativeEvent: MouseEvent | React.PointerEvent<HTMLCanvasElement>;
|
||||
}>,
|
||||
) => {
|
||||
const link = element.link!;
|
||||
const { nativeEvent } = event.detail;
|
||||
const isNewTab = nativeEvent.ctrlKey || nativeEvent.metaKey;
|
||||
const isNewWindow = nativeEvent.shiftKey;
|
||||
const isInternalLink =
|
||||
link.startsWith("/") || link.includes(window.location.origin);
|
||||
if (isInternalLink && !isNewTab && !isNewWindow) {
|
||||
// signal that we're handling the redirect ourselves
|
||||
event.preventDefault();
|
||||
// do a custom redirect, such as passing to react-router
|
||||
// ...
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const onCopy = async (type: "png" | "svg" | "json") => {
|
||||
if (!excalidrawAPI) {
|
||||
return false;
|
||||
}
|
||||
await exportToClipboard({
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
appState: excalidrawAPI.getAppState(),
|
||||
files: excalidrawAPI.getFiles(),
|
||||
type,
|
||||
});
|
||||
window.alert(`Copied to clipboard as ${type} successfully`);
|
||||
};
|
||||
|
||||
const [pointerData, setPointerData] = useState<{
|
||||
pointer: { x: number; y: number };
|
||||
button: "down" | "up";
|
||||
pointersMap: Gesture["pointers"];
|
||||
} | null>(null);
|
||||
|
||||
const onPointerDown = (
|
||||
activeTool: AppState["activeTool"],
|
||||
pointerDownState: ExcalidrawPointerDownState,
|
||||
) => {
|
||||
if (activeTool.type === "custom" && activeTool.customType === "comment") {
|
||||
const { x, y } = pointerDownState.origin;
|
||||
setComment({ x, y, value: "" });
|
||||
}
|
||||
};
|
||||
|
||||
const rerenderCommentIcons = () => {
|
||||
if (!excalidrawAPI) {
|
||||
return false;
|
||||
}
|
||||
const commentIconsElements = appRef.current.querySelectorAll(
|
||||
".comment-icon",
|
||||
) as HTMLElement[];
|
||||
commentIconsElements.forEach((ele) => {
|
||||
const id = ele.id;
|
||||
const appstate = excalidrawAPI.getAppState();
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: commentIcons[id].x, sceneY: commentIcons[id].y },
|
||||
appstate,
|
||||
);
|
||||
ele.style.left = `${
|
||||
x - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetLeft
|
||||
}px`;
|
||||
ele.style.top = `${
|
||||
y - COMMENT_ICON_DIMENSION / 2 - appstate!.offsetTop
|
||||
}px`;
|
||||
});
|
||||
};
|
||||
|
||||
const onPointerMoveFromPointerDownHandler = (
|
||||
pointerDownState: PointerDownState,
|
||||
) => {
|
||||
return withBatchedUpdatesThrottled((event) => {
|
||||
if (!excalidrawAPI) {
|
||||
return false;
|
||||
}
|
||||
const { x, y } = viewportCoordsToSceneCoords(
|
||||
{
|
||||
clientX: event.clientX - pointerDownState.hitElementOffsets.x,
|
||||
clientY: event.clientY - pointerDownState.hitElementOffsets.y,
|
||||
},
|
||||
excalidrawAPI.getAppState(),
|
||||
);
|
||||
setCommentIcons({
|
||||
...commentIcons,
|
||||
[pointerDownState.hitElement.id!]: {
|
||||
...commentIcons[pointerDownState.hitElement.id!],
|
||||
x,
|
||||
y,
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
const onPointerUpFromPointerDownHandler = (
|
||||
pointerDownState: PointerDownState,
|
||||
) => {
|
||||
return withBatchedUpdates((event) => {
|
||||
window.removeEventListener(EVENT.POINTER_MOVE, pointerDownState.onMove);
|
||||
window.removeEventListener(EVENT.POINTER_UP, pointerDownState.onUp);
|
||||
excalidrawAPI?.setActiveTool({ type: "selection" });
|
||||
const distance = distance2d(
|
||||
pointerDownState.x,
|
||||
pointerDownState.y,
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
);
|
||||
if (distance === 0) {
|
||||
if (!comment) {
|
||||
setComment({
|
||||
x: pointerDownState.hitElement.x + 60,
|
||||
y: pointerDownState.hitElement.y,
|
||||
value: pointerDownState.hitElement.value,
|
||||
id: pointerDownState.hitElement.id,
|
||||
});
|
||||
} else {
|
||||
setComment(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const renderCommentIcons = () => {
|
||||
return Object.values(commentIcons).map((commentIcon) => {
|
||||
if (!excalidrawAPI) {
|
||||
return false;
|
||||
}
|
||||
const appState = excalidrawAPI.getAppState();
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: commentIcon.x, sceneY: commentIcon.y },
|
||||
excalidrawAPI.getAppState(),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
id={commentIcon.id}
|
||||
key={commentIcon.id}
|
||||
style={{
|
||||
top: `${y - COMMENT_ICON_DIMENSION / 2 - appState!.offsetTop}px`,
|
||||
left: `${x - COMMENT_ICON_DIMENSION / 2 - appState!.offsetLeft}px`,
|
||||
position: "absolute",
|
||||
zIndex: 1,
|
||||
width: `${COMMENT_ICON_DIMENSION}px`,
|
||||
height: `${COMMENT_ICON_DIMENSION}px`,
|
||||
cursor: "pointer",
|
||||
touchAction: "none",
|
||||
}}
|
||||
className="comment-icon"
|
||||
onPointerDown={(event) => {
|
||||
event.preventDefault();
|
||||
if (comment) {
|
||||
commentIcon.value = comment.value;
|
||||
saveComment();
|
||||
}
|
||||
const pointerDownState: any = {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
hitElement: commentIcon,
|
||||
hitElementOffsets: { x: event.clientX - x, y: event.clientY - y },
|
||||
};
|
||||
const onPointerMove =
|
||||
onPointerMoveFromPointerDownHandler(pointerDownState);
|
||||
const onPointerUp =
|
||||
onPointerUpFromPointerDownHandler(pointerDownState);
|
||||
window.addEventListener(EVENT.POINTER_MOVE, onPointerMove);
|
||||
window.addEventListener(EVENT.POINTER_UP, onPointerUp);
|
||||
|
||||
pointerDownState.onMove = onPointerMove;
|
||||
pointerDownState.onUp = onPointerUp;
|
||||
|
||||
excalidrawAPI?.setActiveTool({
|
||||
type: "custom",
|
||||
customType: "comment",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div className="comment-avatar">
|
||||
<img src="images/doremon.png" alt="doremon" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const saveComment = () => {
|
||||
if (!comment) {
|
||||
return;
|
||||
}
|
||||
if (!comment.id && !comment.value) {
|
||||
setComment(null);
|
||||
return;
|
||||
}
|
||||
const id = comment.id || nanoid();
|
||||
setCommentIcons({
|
||||
...commentIcons,
|
||||
[id]: {
|
||||
x: comment.id ? comment.x - 60 : comment.x,
|
||||
y: comment.y,
|
||||
id,
|
||||
value: comment.value,
|
||||
},
|
||||
});
|
||||
setComment(null);
|
||||
};
|
||||
|
||||
const renderComment = () => {
|
||||
if (!comment) {
|
||||
return null;
|
||||
}
|
||||
const appState = excalidrawAPI?.getAppState()!;
|
||||
const { x, y } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: comment.x, sceneY: comment.y },
|
||||
appState,
|
||||
);
|
||||
let top = y - COMMENT_ICON_DIMENSION / 2 - appState.offsetTop;
|
||||
let left = x - COMMENT_ICON_DIMENSION / 2 - appState.offsetLeft;
|
||||
|
||||
if (
|
||||
top + COMMENT_INPUT_HEIGHT <
|
||||
appState.offsetTop + COMMENT_INPUT_HEIGHT
|
||||
) {
|
||||
top = COMMENT_ICON_DIMENSION / 2;
|
||||
}
|
||||
if (top + COMMENT_INPUT_HEIGHT > appState.height) {
|
||||
top = appState.height - COMMENT_INPUT_HEIGHT - COMMENT_ICON_DIMENSION / 2;
|
||||
}
|
||||
if (
|
||||
left + COMMENT_INPUT_WIDTH <
|
||||
appState.offsetLeft + COMMENT_INPUT_WIDTH
|
||||
) {
|
||||
left = COMMENT_ICON_DIMENSION / 2;
|
||||
}
|
||||
if (left + COMMENT_INPUT_WIDTH > appState.width) {
|
||||
left = appState.width - COMMENT_INPUT_WIDTH - COMMENT_ICON_DIMENSION / 2;
|
||||
}
|
||||
|
||||
return (
|
||||
<textarea
|
||||
className="comment"
|
||||
style={{
|
||||
top: `${top}px`,
|
||||
left: `${left}px`,
|
||||
position: "absolute",
|
||||
zIndex: 1,
|
||||
height: `${COMMENT_INPUT_HEIGHT}px`,
|
||||
width: `${COMMENT_INPUT_WIDTH}px`,
|
||||
}}
|
||||
ref={(ref) => {
|
||||
setTimeout(() => ref?.focus());
|
||||
}}
|
||||
placeholder={comment.value ? "Reply" : "Comment"}
|
||||
value={comment.value}
|
||||
onChange={(event) => {
|
||||
setComment({ ...comment, value: event.target.value });
|
||||
}}
|
||||
onBlur={saveComment}
|
||||
onKeyDown={(event) => {
|
||||
if (!event.shiftKey && event.key === KEYS.ENTER) {
|
||||
event.preventDefault();
|
||||
saveComment();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenu = () => {
|
||||
return (
|
||||
<MainMenu>
|
||||
<MainMenu.DefaultItems.SaveAsImage />
|
||||
<MainMenu.DefaultItems.Export />
|
||||
<MainMenu.Separator />
|
||||
<MainMenu.DefaultItems.LiveCollaborationTrigger
|
||||
isCollaborating={isCollaborating}
|
||||
onSelect={() => window.alert("You clicked on collab button")}
|
||||
/>
|
||||
<MainMenu.Group title="Excalidraw links">
|
||||
<MainMenu.DefaultItems.Socials />
|
||||
</MainMenu.Group>
|
||||
<MainMenu.Separator />
|
||||
<MainMenu.ItemCustom>
|
||||
<button
|
||||
style={{ height: "2rem" }}
|
||||
onClick={() => window.alert("custom menu item")}
|
||||
>
|
||||
custom item
|
||||
</button>
|
||||
</MainMenu.ItemCustom>
|
||||
<MainMenu.DefaultItems.Help />
|
||||
|
||||
{excalidrawAPI && <MobileFooter excalidrawAPI={excalidrawAPI} />}
|
||||
</MainMenu>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="App" ref={appRef}>
|
||||
<h1>{appTitle}</h1>
|
||||
{/* TODO fix type */}
|
||||
<ExampleSidebar>
|
||||
<div className="button-wrapper">
|
||||
<button onClick={loadSceneOrLibrary}>Load Scene or Library</button>
|
||||
<button className="update-scene" onClick={updateScene}>
|
||||
Update Scene
|
||||
</button>
|
||||
<button
|
||||
className="reset-scene"
|
||||
onClick={() => {
|
||||
excalidrawAPI?.resetScene();
|
||||
}}
|
||||
>
|
||||
Reset Scene
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const libraryItems: LibraryItems = [
|
||||
{
|
||||
status: "published",
|
||||
id: "1",
|
||||
created: 1,
|
||||
elements: initialData.libraryItems[1] as any,
|
||||
},
|
||||
{
|
||||
status: "unpublished",
|
||||
id: "2",
|
||||
created: 2,
|
||||
elements: initialData.libraryItems[1] as any,
|
||||
},
|
||||
];
|
||||
excalidrawAPI?.updateLibrary({
|
||||
libraryItems,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Update Library
|
||||
</button>
|
||||
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={viewModeEnabled}
|
||||
onChange={() => setViewModeEnabled(!viewModeEnabled)}
|
||||
/>
|
||||
View mode
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={zenModeEnabled}
|
||||
onChange={() => setZenModeEnabled(!zenModeEnabled)}
|
||||
/>
|
||||
Zen mode
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={gridModeEnabled}
|
||||
onChange={() => setGridModeEnabled(!gridModeEnabled)}
|
||||
/>
|
||||
Grid mode
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={theme === "dark"}
|
||||
onChange={() => {
|
||||
setTheme(theme === "light" ? "dark" : "light");
|
||||
}}
|
||||
/>
|
||||
Switch to Dark Theme
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={disableImageTool === true}
|
||||
onChange={() => {
|
||||
setDisableImageTool(!disableImageTool);
|
||||
}}
|
||||
/>
|
||||
Disable Image Tool
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isCollaborating}
|
||||
onChange={() => {
|
||||
if (!isCollaborating) {
|
||||
const collaborators = new Map();
|
||||
collaborators.set("id1", {
|
||||
username: "Doremon",
|
||||
avatarUrl: "images/doremon.png",
|
||||
});
|
||||
collaborators.set("id2", {
|
||||
username: "Excalibot",
|
||||
avatarUrl: "images/excalibot.png",
|
||||
});
|
||||
collaborators.set("id3", {
|
||||
username: "Pika",
|
||||
avatarUrl: "images/pika.jpeg",
|
||||
});
|
||||
collaborators.set("id4", {
|
||||
username: "fallback",
|
||||
avatarUrl: "https://example.com",
|
||||
});
|
||||
excalidrawAPI?.updateScene({ collaborators });
|
||||
} else {
|
||||
excalidrawAPI?.updateScene({
|
||||
collaborators: new Map(),
|
||||
});
|
||||
}
|
||||
setIsCollaborating(!isCollaborating);
|
||||
}}
|
||||
/>
|
||||
Show collaborators
|
||||
</label>
|
||||
<div>
|
||||
<button onClick={onCopy.bind(null, "png")}>
|
||||
Copy to Clipboard as PNG
|
||||
</button>
|
||||
<button onClick={onCopy.bind(null, "svg")}>
|
||||
Copy to Clipboard as SVG
|
||||
</button>
|
||||
<button onClick={onCopy.bind(null, "json")}>
|
||||
Copy to Clipboard as JSON
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: "1em",
|
||||
justifyContent: "center",
|
||||
marginTop: "1em",
|
||||
}}
|
||||
>
|
||||
<div>x: {pointerData?.pointer.x ?? 0}</div>
|
||||
<div>y: {pointerData?.pointer.y ?? 0}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="excalidraw-wrapper">
|
||||
<Excalidraw
|
||||
excalidrawAPI={(api: ExcalidrawImperativeAPI) =>
|
||||
setExcalidrawAPI(api)
|
||||
}
|
||||
initialData={initialStatePromiseRef.current.promise}
|
||||
onChange={(elements, state) => {
|
||||
// console.info("Elements :", elements, "State : ", state);
|
||||
}}
|
||||
onPointerUpdate={(payload: {
|
||||
pointer: { x: number; y: number };
|
||||
button: "down" | "up";
|
||||
pointersMap: Gesture["pointers"];
|
||||
}) => setPointerData(payload)}
|
||||
viewModeEnabled={viewModeEnabled}
|
||||
zenModeEnabled={zenModeEnabled}
|
||||
gridModeEnabled={gridModeEnabled}
|
||||
theme={theme}
|
||||
name="Custom name of drawing"
|
||||
UIOptions={{
|
||||
canvasActions: {
|
||||
loadScene: false,
|
||||
},
|
||||
tools: { image: !disableImageTool },
|
||||
}}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onScrollChange={rerenderCommentIcons}
|
||||
// allow all urls
|
||||
validateEmbeddable={true}
|
||||
>
|
||||
{excalidrawAPI && (
|
||||
<Footer>
|
||||
<CustomFooter excalidrawAPI={excalidrawAPI} />
|
||||
</Footer>
|
||||
)}
|
||||
<WelcomeScreen />
|
||||
<Sidebar name="custom">
|
||||
<Sidebar.Tabs>
|
||||
<Sidebar.Header />
|
||||
<Sidebar.Tab tab="one">Tab one!</Sidebar.Tab>
|
||||
<Sidebar.Tab tab="two">Tab two!</Sidebar.Tab>
|
||||
<Sidebar.TabTriggers>
|
||||
<Sidebar.TabTrigger tab="one">One</Sidebar.TabTrigger>
|
||||
<Sidebar.TabTrigger tab="two">Two</Sidebar.TabTrigger>
|
||||
</Sidebar.TabTriggers>
|
||||
</Sidebar.Tabs>
|
||||
</Sidebar>
|
||||
<Sidebar.Trigger
|
||||
name="custom"
|
||||
tab="one"
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
bottom: "20px",
|
||||
zIndex: 9999999999999999,
|
||||
}}
|
||||
>
|
||||
Toggle Custom Sidebar
|
||||
</Sidebar.Trigger>
|
||||
{renderMenu()}
|
||||
{excalidrawAPI && (
|
||||
<TTDDialogTrigger icon={<span>😀</span>}>
|
||||
Text to diagram
|
||||
</TTDDialogTrigger>
|
||||
)}
|
||||
<TTDDialog
|
||||
onTextSubmit={async (_) => {
|
||||
console.info("submit");
|
||||
// sleep for 2s
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
throw new Error("error, go away now");
|
||||
// return "dummy";
|
||||
}}
|
||||
/>
|
||||
</Excalidraw>
|
||||
{Object.keys(commentIcons || []).length > 0 && renderCommentIcons()}
|
||||
{comment && renderComment()}
|
||||
</div>
|
||||
|
||||
<div className="export-wrapper button-wrapper">
|
||||
<label className="export-wrapper__checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={exportWithDarkMode}
|
||||
onChange={() => setExportWithDarkMode(!exportWithDarkMode)}
|
||||
/>
|
||||
Export with dark mode
|
||||
</label>
|
||||
<label className="export-wrapper__checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={exportEmbedScene}
|
||||
onChange={() => setExportEmbedScene(!exportEmbedScene)}
|
||||
/>
|
||||
Export with embed scene
|
||||
</label>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
const svg = await exportToSvg({
|
||||
elements: excalidrawAPI?.getSceneElements(),
|
||||
appState: {
|
||||
...initialData.appState,
|
||||
exportWithDarkMode,
|
||||
exportEmbedScene,
|
||||
width: 300,
|
||||
height: 100,
|
||||
},
|
||||
files: excalidrawAPI?.getFiles(),
|
||||
});
|
||||
appRef.current.querySelector(".export-svg").innerHTML =
|
||||
svg.outerHTML;
|
||||
}}
|
||||
>
|
||||
Export to SVG
|
||||
</button>
|
||||
<div className="export export-svg"></div>
|
||||
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
const blob = await exportToBlob({
|
||||
elements: excalidrawAPI?.getSceneElements(),
|
||||
mimeType: "image/png",
|
||||
appState: {
|
||||
...initialData.appState,
|
||||
exportEmbedScene,
|
||||
exportWithDarkMode,
|
||||
},
|
||||
files: excalidrawAPI?.getFiles(),
|
||||
});
|
||||
setBlobUrl(window.URL.createObjectURL(blob));
|
||||
}}
|
||||
>
|
||||
Export to Blob
|
||||
</button>
|
||||
<div className="export export-blob">
|
||||
<img src={blobUrl} alt="" />
|
||||
</div>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
const canvas = await exportToCanvas({
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
appState: {
|
||||
...initialData.appState,
|
||||
exportWithDarkMode,
|
||||
},
|
||||
files: excalidrawAPI.getFiles(),
|
||||
});
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.font = "30px Virgil";
|
||||
ctx.strokeText("My custom text", 50, 60);
|
||||
setCanvasUrl(canvas.toDataURL());
|
||||
}}
|
||||
>
|
||||
Export to Canvas
|
||||
</button>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
const canvas = await exportToCanvas({
|
||||
elements: excalidrawAPI.getSceneElements(),
|
||||
appState: {
|
||||
...initialData.appState,
|
||||
exportWithDarkMode,
|
||||
},
|
||||
files: excalidrawAPI.getFiles(),
|
||||
});
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.font = "30px Virgil";
|
||||
ctx.strokeText("My custom text", 50, 60);
|
||||
setCanvasUrl(canvas.toDataURL());
|
||||
}}
|
||||
>
|
||||
Export to Canvas
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = excalidrawAPI.getSceneElements();
|
||||
excalidrawAPI.scrollToContent(elements[0], {
|
||||
fitToViewport: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Fit to viewport, first element
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = excalidrawAPI.getSceneElements();
|
||||
excalidrawAPI.scrollToContent(elements[0], {
|
||||
fitToContent: true,
|
||||
});
|
||||
|
||||
excalidrawAPI.scrollToContent(elements[0], {
|
||||
fitToContent: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Fit to content, first element
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
const elements = excalidrawAPI.getSceneElements();
|
||||
excalidrawAPI.scrollToContent(elements[0], {
|
||||
fitToContent: true,
|
||||
});
|
||||
|
||||
excalidrawAPI.scrollToContent(elements[0]);
|
||||
}}
|
||||
>
|
||||
Scroll to first element, no fitToContent, no fitToViewport
|
||||
</button>
|
||||
<div className="export export-canvas">
|
||||
<img src={canvasUrl} alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</ExampleSidebar>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import type { ExcalidrawImperativeAPI } from "../types";
|
||||
|
||||
const { Button, MIME_TYPES } = window.ExcalidrawLib;
|
||||
const COMMENT_SVG = (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="feather feather-message-circle"
|
||||
>
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||
</svg>
|
||||
);
|
||||
const CustomFooter = ({
|
||||
excalidrawAPI,
|
||||
}: {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onSelect={() => alert("General Kenobi!")}
|
||||
className="you are a bold one"
|
||||
style={{ marginLeft: "1rem" }}
|
||||
title="Hello there!"
|
||||
>
|
||||
{COMMENT_SVG}
|
||||
</Button>
|
||||
<button
|
||||
className="custom-element"
|
||||
onClick={() => {
|
||||
excalidrawAPI?.setActiveTool({
|
||||
type: "custom",
|
||||
customType: "comment",
|
||||
});
|
||||
const url = `data:${MIME_TYPES.svg},${encodeURIComponent(
|
||||
`<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="feather feather-message-circle"
|
||||
>
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"></path>
|
||||
</svg>`,
|
||||
)}`;
|
||||
excalidrawAPI?.setCursor(`url(${url}), auto`);
|
||||
}}
|
||||
>
|
||||
{COMMENT_SVG}
|
||||
</button>
|
||||
<button
|
||||
className="custom-footer"
|
||||
onClick={() => alert("This is dummy footer")}
|
||||
>
|
||||
custom footer
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomFooter;
|
|
@ -1,20 +0,0 @@
|
|||
import type { ExcalidrawImperativeAPI } from "../types";
|
||||
import CustomFooter from "./CustomFooter";
|
||||
const { useDevice, Footer } = window.ExcalidrawLib;
|
||||
|
||||
const MobileFooter = ({
|
||||
excalidrawAPI,
|
||||
}: {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI;
|
||||
}) => {
|
||||
const device = useDevice();
|
||||
if (device.editor.isMobile) {
|
||||
return (
|
||||
<Footer>
|
||||
<CustomFooter excalidrawAPI={excalidrawAPI} />
|
||||
</Footer>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
export default MobileFooter;
|
|
@ -1,17 +0,0 @@
|
|||
import App from "./App";
|
||||
|
||||
const { StrictMode } = window.React;
|
||||
//@ts-ignore
|
||||
const { createRoot } = window.ReactDOM;
|
||||
|
||||
const rootElement = document.getElementById("root")!;
|
||||
const root = createRoot(rootElement);
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App
|
||||
appTitle={"Excalidraw Example"}
|
||||
useCustom={(api: any, args?: any[]) => {}}
|
||||
/>
|
||||
</StrictMode>,
|
||||
);
|
|
@ -1,994 +0,0 @@
|
|||
import type { ExcalidrawElementSkeleton } from "../data/transform";
|
||||
import type { FileId } from "../element/types";
|
||||
|
||||
const elements: ExcalidrawElementSkeleton[] = [
|
||||
{
|
||||
type: "rectangle",
|
||||
x: 10,
|
||||
y: 10,
|
||||
strokeWidth: 2,
|
||||
id: "1",
|
||||
},
|
||||
{
|
||||
type: "diamond",
|
||||
x: 120,
|
||||
y: 20,
|
||||
backgroundColor: "#fff3bf",
|
||||
strokeWidth: 2,
|
||||
label: {
|
||||
text: "HELLO EXCALIDRAW",
|
||||
strokeColor: "#099268",
|
||||
fontSize: 30,
|
||||
},
|
||||
id: "2",
|
||||
},
|
||||
{
|
||||
type: "arrow",
|
||||
x: 100,
|
||||
y: 200,
|
||||
label: { text: "HELLO WORLD!!" },
|
||||
start: { type: "rectangle" },
|
||||
end: { type: "ellipse" },
|
||||
},
|
||||
{
|
||||
type: "image",
|
||||
x: 606.1042326312408,
|
||||
y: 153.57729779411773,
|
||||
width: 230,
|
||||
height: 230,
|
||||
fileId: "rocket" as FileId,
|
||||
},
|
||||
{
|
||||
type: "frame",
|
||||
children: ["1", "2"],
|
||||
name: "My frame",
|
||||
},
|
||||
];
|
||||
export default {
|
||||
elements,
|
||||
appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 1 },
|
||||
scrollToContent: true,
|
||||
libraryItems: [
|
||||
[
|
||||
{
|
||||
type: "line",
|
||||
|
||||
x: 209.72304760646858,
|
||||
y: 338.83587294718825,
|
||||
strokeColor: "#881fa3",
|
||||
backgroundColor: "#be4bdb",
|
||||
width: 116.42036295658873,
|
||||
height: 103.65107323746608,
|
||||
strokeSharpness: "sharp",
|
||||
points: [
|
||||
[-92.28090097254909, 7.105427357601002e-15],
|
||||
[-154.72281841151394, 19.199290805487394],
|
||||
[-155.45758928571422, 79.43840749607878],
|
||||
[-99.89923520113778, 103.6510732374661],
|
||||
[-40.317783799181804, 79.1587107641305],
|
||||
[-39.037226329125524, 21.285677238400705],
|
||||
[-92.28090097254909, 7.105427357601002e-15],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: "line",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
x: -249.48446738689245,
|
||||
y: 374.851387389359,
|
||||
strokeColor: "#0a11d3",
|
||||
backgroundColor: "#228be6",
|
||||
width: 88.21658171083376,
|
||||
height: 113.8575037534261,
|
||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||
points: [
|
||||
[-0.22814350714115691, -43.414939319563715],
|
||||
[0.06274947619197979, 42.63794490105306],
|
||||
[-0.21453039840335475, 52.43469208825097],
|
||||
[4.315205554872581, 56.66774540453215],
|
||||
[20.089784992984285, 60.25027917349701],
|
||||
[46.7532926683984, 61.365826671969444],
|
||||
[72.22851104292477, 59.584691681394986],
|
||||
[85.76368213524371, 55.325139565662596],
|
||||
[87.67263486434864, 51.7342924478499],
|
||||
[87.94074036468018, 43.84700272879395],
|
||||
[87.73030872197806, -36.195582644606276],
|
||||
[87.2559282533682, -43.758132174307036],
|
||||
[81.5915337527493, -47.984890854524416],
|
||||
[69.66352776578219, -50.4328058257654],
|
||||
[42.481213744224995, -52.49167708145666],
|
||||
[20.68789182864576, -51.26396751574663],
|
||||
[3.5475921483286084, -47.099726468136254],
|
||||
[-0.2758413461535838, -43.46664538034193],
|
||||
[-0.22814350714115691, -43.414939319563715],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -249.02524930453623,
|
||||
y: 398.8804363713438,
|
||||
strokeColor: "#0a11d3",
|
||||
backgroundColor: "transparent",
|
||||
width: 88.30808627974527,
|
||||
height: 9.797916664247975,
|
||||
seed: 683951089,
|
||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, -2.1538602707609424],
|
||||
[2.326538897826852, 1.751753055375216],
|
||||
[12.359939318521995, 5.028526743934819],
|
||||
[25.710950037209347, 7.012921076245119],
|
||||
[46.6269757640547, 7.193749997581346],
|
||||
[71.03526003420632, 5.930375670950649],
|
||||
[85.2899738827162, 1.3342483900732343],
|
||||
[88.30808627974527, -2.6041666666666288],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -250.11899081659772,
|
||||
y: 365.80628180927204,
|
||||
strokeColor: "#0a11d3",
|
||||
backgroundColor: "transparent",
|
||||
width: 88.30808627974527,
|
||||
height: 9.797916664247975,
|
||||
seed: 1817746897,
|
||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, -2.1538602707609424],
|
||||
[2.326538897826852, 1.751753055375216],
|
||||
[12.359939318521995, 5.028526743934819],
|
||||
[25.710950037209347, 7.012921076245119],
|
||||
[46.6269757640547, 7.193749997581346],
|
||||
[71.03526003420632, 5.930375670950649],
|
||||
[85.2899738827162, 1.3342483900732343],
|
||||
[88.30808627974527, -2.6041666666666288],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -251.23981350275943,
|
||||
y: 323.4117518426986,
|
||||
strokeColor: "#0a11d3",
|
||||
backgroundColor: "#fff",
|
||||
width: 87.65074610854188,
|
||||
height: 17.72670397681366,
|
||||
seed: 1409727409,
|
||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||
strokeSharpness: "sharp",
|
||||
boundElementIds: ["bxuMGTzXLn7H-uBCptINx"],
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -179.73008120217884,
|
||||
y: 347.98755471983213,
|
||||
strokeColor: "#0a11d3",
|
||||
backgroundColor: "#fff",
|
||||
width: 12.846057046979809,
|
||||
height: 13.941904362416096,
|
||||
seed: 1073094033,
|
||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -179.73008120217884,
|
||||
y: 378.5900085788926,
|
||||
strokeColor: "#0a11d3",
|
||||
backgroundColor: "#fff",
|
||||
width: 12.846057046979809,
|
||||
height: 13.941904362416096,
|
||||
seed: 526271345,
|
||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -179.73008120217884,
|
||||
y: 411.8508097533892,
|
||||
strokeColor: "#0a11d3",
|
||||
backgroundColor: "#fff",
|
||||
width: 12.846057046979809,
|
||||
height: 13.941904362416096,
|
||||
seed: 243707217,
|
||||
groupIds: ["N2YAi9nU-wlRb0rDaDZoe"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: "diamond",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -109.55894395256101,
|
||||
y: 381.22641397493356,
|
||||
strokeColor: "#c92a2a",
|
||||
backgroundColor: "#fd8888",
|
||||
width: 112.64736525303451,
|
||||
height: 36.77344700318558,
|
||||
seed: 511870335,
|
||||
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "diamond",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -109.55894395256101,
|
||||
y: 372.354634046675,
|
||||
strokeColor: "#c92a2a",
|
||||
backgroundColor: "#fd8888",
|
||||
width: 112.64736525303451,
|
||||
height: 36.77344700318558,
|
||||
seed: 1283079231,
|
||||
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "diamond",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -109.55894395256101,
|
||||
y: 359.72407445196296,
|
||||
strokeColor: "#c92a2a",
|
||||
backgroundColor: "#fd8888",
|
||||
width: 112.64736525303451,
|
||||
height: 36.77344700318558,
|
||||
seed: 996251633,
|
||||
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "diamond",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -109.55894395256101,
|
||||
y: 347.1924021546656,
|
||||
strokeColor: "#c92a2a",
|
||||
backgroundColor: "#fd8888",
|
||||
width: 112.64736525303451,
|
||||
height: 36.77344700318558,
|
||||
seed: 1764842481,
|
||||
groupIds: ["M6ByXuSmtHCr3RtPPKJQh"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: "line",
|
||||
fillStyle: "hachure",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 1.5707963267948957,
|
||||
x: -471.6208001976387,
|
||||
y: 520.7681448415112,
|
||||
strokeColor: "#087f5b",
|
||||
backgroundColor: "#40c057",
|
||||
width: 52.317507746132115,
|
||||
height: 154.56722543646003,
|
||||
seed: 1424381745,
|
||||
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[-0.24755378372925183, -40.169554027464216],
|
||||
[-0.07503751055611152, 76.6515171914404],
|
||||
[-0.23948042713317108, 89.95108885873196],
|
||||
[2.446913573036335, 95.69766931810295],
|
||||
[11.802146636255692, 100.56113713047068],
|
||||
[27.615140546177496, 102.07554835500338],
|
||||
[42.72341054254274, 99.65756899883291],
|
||||
[50.75054563137204, 93.87501510096598],
|
||||
[51.88266441510958, 89.00026150397161],
|
||||
[52.04166639997853, 78.29287333983132],
|
||||
[51.916868330459295, -30.36891819848148],
|
||||
[51.635533423123285, -40.63545540065934],
|
||||
[48.27622163143906, -46.37349057843314],
|
||||
[41.202227904674494, -49.69665692879073],
|
||||
[25.081551986374073, -52.49167708145666],
|
||||
[12.15685839679867, -50.825000270901],
|
||||
[1.9916746648394732, -45.171835889467935],
|
||||
[-0.2758413461535838, -40.23974757720194],
|
||||
[-0.24755378372925183, -40.169554027464216],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
version: 2405,
|
||||
versionNonce: 2120341087,
|
||||
isDeleted: false,
|
||||
id: "TYsYe2VvJ60T_yKa3kyOw",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 1.5707963267948957,
|
||||
x: -496.3957643857249,
|
||||
y: 541.7241190920508,
|
||||
strokeColor: "#087f5b",
|
||||
backgroundColor: "transparent",
|
||||
width: 50.7174766392476,
|
||||
height: 12.698053371678215,
|
||||
seed: 726657713,
|
||||
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, -2.0205717204386002],
|
||||
[1.3361877396713384, 3.0410845646550486],
|
||||
[7.098613049589299, 7.287767671898479],
|
||||
[14.766422451441104, 9.859533283467512],
|
||||
[26.779003528407447, 10.093886705011586],
|
||||
[40.79727342221974, 8.456559589697127],
|
||||
[48.98410145879092, 2.500000505196364],
|
||||
[50.7174766392476, -2.6041666666666288],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 1.5707963267948957,
|
||||
x: -450.969983237283,
|
||||
y: 542.1789894334747,
|
||||
strokeColor: "#087f5b",
|
||||
backgroundColor: "transparent",
|
||||
width: 50.57247907260371,
|
||||
height: 10.178760037658167,
|
||||
seed: 1977326481,
|
||||
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, -2.136356936862347],
|
||||
[1.332367676378171, 1.9210669226078037],
|
||||
[7.078318632616268, 5.325208253515953],
|
||||
[14.724206326638113, 7.386735659885842],
|
||||
[26.70244431044034, 7.574593370991538],
|
||||
[40.68063699304561, 6.262111896696538],
|
||||
[48.84405948536458, 1.4873339211608216],
|
||||
[50.57247907260371, -2.6041666666666288],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 1.5707963267948957,
|
||||
x: -404.36521010516793,
|
||||
y: 534.1894365757241,
|
||||
strokeColor: "#087f5b",
|
||||
backgroundColor: "#fff",
|
||||
width: 51.27812853552538,
|
||||
height: 22.797152568995934,
|
||||
seed: 1774660383,
|
||||
groupIds: ["HSrtfEf-CssQTf160Fb6R"],
|
||||
strokeSharpness: "sharp",
|
||||
boundElementIds: ["bxuMGTzXLn7H-uBCptINx"],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: "rectangle",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 2,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -393.3000561423187,
|
||||
y: 338.9742643666818,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 70.67858069123133,
|
||||
height: 107.25081879410921,
|
||||
seed: 371096063,
|
||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||
strokeSharpness: "sharp",
|
||||
boundElementIds: [
|
||||
"CFu0B4Mw_1wC1Hbgx8Fs0",
|
||||
"XIl_NhaFtRO00pX5Pq6VU",
|
||||
"EndiSTFlx1AT7vcBVjgve",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "rectangle",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 2,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -400.8474891780329,
|
||||
y: 331.95417508096745,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 70.67858069123133,
|
||||
height: 107.25081879410921,
|
||||
seed: 685932433,
|
||||
groupIds: ["0RJwA-yKP5dqk5oMiSeot", "9ppmKFUbA4iKjt8FaDFox"],
|
||||
strokeSharpness: "sharp",
|
||||
boundElementIds: [
|
||||
"CFu0B4Mw_1wC1Hbgx8Fs0",
|
||||
"XIl_NhaFtRO00pX5Pq6VU",
|
||||
"EndiSTFlx1AT7vcBVjgve",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "rectangle",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 2,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -410.24257846374826,
|
||||
y: 323.7002688309677,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 70.67858069123133,
|
||||
height: 107.25081879410921,
|
||||
seed: 58634943,
|
||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||
strokeSharpness: "sharp",
|
||||
boundElementIds: [
|
||||
"CFu0B4Mw_1wC1Hbgx8Fs0",
|
||||
"XIl_NhaFtRO00pX5Pq6VU",
|
||||
"EndiSTFlx1AT7vcBVjgve",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "draw",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -398.2561518768373,
|
||||
y: 371.84603609547054,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 46.57983585730082,
|
||||
height: 3.249953844290203,
|
||||
seed: 1673003743,
|
||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, 0.6014697828497827],
|
||||
[40.42449133807562, 0.7588628355182573],
|
||||
[46.57983585730082, -2.491091008771946],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "draw",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -396.400899638823,
|
||||
y: 340.9822185794818,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 45.567415680676426,
|
||||
height: 2.8032978840147194,
|
||||
seed: 1821527807,
|
||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, 0],
|
||||
[16.832548902953302, -2.8032978840147194],
|
||||
[45.567415680676426, -0.3275477042019195],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "draw",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -396.4774991551924,
|
||||
y: 408.37659284983897,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 48.33668263438425,
|
||||
height: 4.280657518731036,
|
||||
seed: 1485707039,
|
||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, 0],
|
||||
[26.41225578429045, -0.2552319773002338],
|
||||
[37.62000339651456, 2.3153712935189787],
|
||||
[48.33668263438425, -1.9652862252120569],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "draw",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -399.6615463367227,
|
||||
y: 419.61974125811776,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 54.40694982784246,
|
||||
height: 2.9096445412231735,
|
||||
seed: 1042012991,
|
||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, 0],
|
||||
[10.166093050596771, -1.166642430373031],
|
||||
[16.130660965377448, -0.8422655250909383],
|
||||
[46.26079588567538, 0.6125567455206506],
|
||||
[54.40694982784246, -2.297087795702523],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "draw",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -399.3767034411569,
|
||||
y: 356.042820132743,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 46.92865289294453,
|
||||
height: 2.4757501798128,
|
||||
seed: 295443295,
|
||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, 0],
|
||||
[18.193786115221407, -0.5912874140789839],
|
||||
[46.92865289294453, 1.884462765733816],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "draw",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -399.26921524500654,
|
||||
y: 390.5261491685826,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 46.92865289294453,
|
||||
height: 2.4757501798128,
|
||||
seed: 1734301567,
|
||||
groupIds: ["9ppmKFUbA4iKjt8FaDFox"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, 0],
|
||||
[8.093938105125233, 1.4279702913643746],
|
||||
[18.193786115221407, -0.5912874140789839],
|
||||
[46.92865289294453, 1.884462765733816],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: "rectangle",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -593.9896997899341,
|
||||
y: 343.9798351106279,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
width: 127.88383573213892,
|
||||
height: 76.53703389977764,
|
||||
seed: 106569279,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -595.0652975408293,
|
||||
y: 354.6963695028721,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
width: 128.84193229844433,
|
||||
height: 0,
|
||||
seed: 73916127,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, 0],
|
||||
[128.84193229844433, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 0,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -589.5016643209792,
|
||||
y: 348.2514049106367,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fa5252",
|
||||
width: 5.001953125,
|
||||
height: 5.001953125,
|
||||
seed: 387857791,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 0,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -579.2389690084792,
|
||||
y: 348.2514049106367,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fab005",
|
||||
width: 5.001953125,
|
||||
height: 5.001953125,
|
||||
seed: 1486370207,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 0,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -568.525552542133,
|
||||
y: 348.7021260644829,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#40c057",
|
||||
width: 5.001953125,
|
||||
height: 5.001953125,
|
||||
seed: 610150847,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 90,
|
||||
angle: 0,
|
||||
x: -552.4984915525058,
|
||||
y: 364.75449494249875,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#04aaf7",
|
||||
width: 42.72020253937572,
|
||||
height: 42.72020253937572,
|
||||
seed: 144280593,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "draw",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
x: -530.327851842306,
|
||||
y: 378.9357912947449,
|
||||
strokeColor: "#087f5b",
|
||||
backgroundColor: "#40c057",
|
||||
width: 28.226201983883442,
|
||||
height: 24.44112284281997,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "round",
|
||||
points: [
|
||||
[4.907524351775825, 2.043055712211473],
|
||||
[3.0769604829149455, 1.6284171290602836],
|
||||
[-2.66472604008681, -4.228569719133945],
|
||||
[-6.450168189798415, -2.304577297733668],
|
||||
[-7.704241049212052, 4.416384506147983],
|
||||
[-6.361372181234263, 8.783101300254884],
|
||||
[-12.516984713388897, 10.9291595737194],
|
||||
[-12.295677738198286, 15.686226498407976],
|
||||
[-7.473371426945252, 15.393030178104425],
|
||||
[-3.787654025313423, 11.5207568827343],
|
||||
[1.2873793872375165, 19.910682356036197],
|
||||
[4.492232250183542, 20.212553123686025],
|
||||
[1.1302787567009416, 6.843494873631317],
|
||||
[6.294108177816019, 6.390688722156585],
|
||||
[8.070028349098962, 7.910451897221202],
|
||||
[14.143675334886687, 7.910451897221202],
|
||||
[15.709217270494545, 2.6780252579576427],
|
||||
[9.128749989671498, 3.1533849725326517],
|
||||
[10.393751588600717, -3.7167773257046695],
|
||||
[7.380151667177483, -3.30213874255348],
|
||||
[4.669824267311791, 1.1200945145694894],
|
||||
[4.907524351775825, 2.043055712211473],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 90,
|
||||
angle: 0,
|
||||
x: -551.4394290784783,
|
||||
y: 385.71736850567976,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#99bcff",
|
||||
width: 42.095115772272244,
|
||||
height: 0,
|
||||
seed: 1443027377,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, 0],
|
||||
[42.095115772272244, 0],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 0,
|
||||
opacity: 90,
|
||||
angle: 0,
|
||||
x: -546.3441000487039,
|
||||
y: 372.6245229061568,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#99bcff",
|
||||
width: 29.31860660384862,
|
||||
height: 5.711199931375845,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "round",
|
||||
points: [
|
||||
[0, -2.341683327443203],
|
||||
[0.7724193963150375, -0.06510358900749044],
|
||||
[4.103544916365185, 1.84492589414448],
|
||||
[8.536129150893453, 3.0016281808630056],
|
||||
[15.480325949120388, 3.1070332647092163],
|
||||
[23.583965316012858, 2.3706131055211244],
|
||||
[28.316582284417855, -0.3084668090492442],
|
||||
[29.31860660384862, -2.6041666666666288],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 90,
|
||||
angle: 0,
|
||||
x: -538.2701841247845,
|
||||
y: 363.37196531290607,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "transparent",
|
||||
width: 15.528434353116108,
|
||||
height: 44.82230388130942,
|
||||
seed: 683572113,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
opacity: 90,
|
||||
x: -544.828148539078,
|
||||
y: 402.0199316371545,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#99bcff",
|
||||
width: 29.31860660384862,
|
||||
height: 5.896061363392446,
|
||||
seed: 318798801,
|
||||
groupIds: ["TC0RSM64Cxmu17MlE12-o"],
|
||||
strokeSharpness: "round",
|
||||
|
||||
points: [
|
||||
[0, 0],
|
||||
[4.103544916365185, -4.322122351104391],
|
||||
[8.536129150893453, -5.516265043290966],
|
||||
[15.480325949120388, -5.625081903117008],
|
||||
[23.583965316012858, -4.8648251269605955],
|
||||
[28.316582284417855, -2.0990281379671547],
|
||||
[29.31860660384862, 0.2709794602754383],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: "rectangle",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -715.1043446306466,
|
||||
y: 330.4231266309418,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#ced4da",
|
||||
width: 70.81644178885557,
|
||||
height: 108.30428902193904,
|
||||
seed: 1914896753,
|
||||
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "rectangle",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -706.996640540555,
|
||||
y: 338.68030798133873,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 55.801163535143246,
|
||||
height: 82.83278895375764,
|
||||
seed: 1306468145,
|
||||
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "ellipse",
|
||||
fillStyle: "solid",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -684.8099707762028,
|
||||
y: 425.0579911039235,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fff",
|
||||
width: 11.427824006438863,
|
||||
height: 11.427824006438863,
|
||||
seed: 93422161,
|
||||
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "rectangle",
|
||||
fillStyle: "cross-hatch",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
roughness: 1,
|
||||
opacity: 100,
|
||||
angle: 0,
|
||||
x: -698.7169501405845,
|
||||
y: 349.2244646574789,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fab005",
|
||||
width: 39.2417827352022,
|
||||
height: 19.889460471185775,
|
||||
seed: 11646495,
|
||||
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
{
|
||||
type: "rectangle",
|
||||
fillStyle: "cross-hatch",
|
||||
strokeWidth: 1,
|
||||
strokeStyle: "solid",
|
||||
x: -698.7169501405845,
|
||||
y: 384.7822247024333,
|
||||
strokeColor: "#000000",
|
||||
backgroundColor: "#fab005",
|
||||
width: 39.2417827352022,
|
||||
height: 19.889460471185775,
|
||||
seed: 291717649,
|
||||
groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"],
|
||||
strokeSharpness: "sharp",
|
||||
},
|
||||
],
|
||||
],
|
||||
};
|
Binary file not shown.
Before Width: | Height: | Size: 197 KiB |
Binary file not shown.
Before Width: | Height: | Size: 30 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 39 KiB |
|
@ -1,32 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no"
|
||||
/>
|
||||
<meta name="theme-color" content="#000000" />
|
||||
|
||||
<title>React App</title>
|
||||
<script>
|
||||
window.name = "codesandbox";
|
||||
</script>
|
||||
<link rel="stylesheet" href="/dist/browser/dev/index.css" />
|
||||
<link rel="stylesheet" href="bundle.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript> You need to enable JavaScript to run this app. </noscript>
|
||||
<div id="root"></div>
|
||||
<script src="https://unpkg.com/react@18.2.0/umd/react.development.js"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.development.js"></script>
|
||||
<!-- This is so that we use the bundled excalidraw.development.js file instead
|
||||
of the actual source code -->
|
||||
<script type="module">
|
||||
import * as ExcalidrawLib from "/dist/browser/dev/index.js";
|
||||
window.ExcalidrawLib = ExcalidrawLib;
|
||||
</script>
|
||||
<script type="module" src="bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,66 +0,0 @@
|
|||
.sidebar {
|
||||
height: 100%;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: #111;
|
||||
overflow-x: hidden;
|
||||
transition: 0.5s;
|
||||
padding-top: 60px;
|
||||
|
||||
&.open {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
&-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
margin: 10px;
|
||||
background: #faa2c1;
|
||||
color: #fff;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar a {
|
||||
padding: 8px 8px 8px 32px;
|
||||
text-decoration: none;
|
||||
font-size: 25px;
|
||||
color: #818181;
|
||||
display: block;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.sidebar a:hover {
|
||||
color: #f1f1f1;
|
||||
}
|
||||
|
||||
.sidebar .closebtn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
font-size: 36px;
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.openbtn {
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
background-color: #111;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
display: flex;
|
||||
margin-left: 50px;
|
||||
}
|
||||
.sidebar-open {
|
||||
margin-left: 300px;
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
import "./ExampleSidebar.scss";
|
||||
|
||||
const React = window.React;
|
||||
|
||||
export default function Sidebar({ children }: { children: React.ReactNode }) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="mySidebar" className={`sidebar ${open ? "open" : ""}`}>
|
||||
<button className="closebtn" onClick={() => setOpen(false)}>
|
||||
x
|
||||
</button>
|
||||
<div className="sidebar-links">
|
||||
<button>Empty Home</button>
|
||||
<button>Empty About</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${open ? "sidebar-open" : ""}`}>
|
||||
<button
|
||||
className="openbtn"
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
>
|
||||
Open Sidebar
|
||||
</button>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -4,6 +4,8 @@ import {
|
|||
isTextElement,
|
||||
} from "./element";
|
||||
import {
|
||||
ElementsMap,
|
||||
ElementsMapOrArray,
|
||||
ExcalidrawElement,
|
||||
ExcalidrawFrameLikeElement,
|
||||
NonDeleted,
|
||||
|
@ -21,8 +23,12 @@ import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
|||
import { getElementsInGroup, selectGroupsFromGivenElements } from "./groups";
|
||||
import Scene, { ExcalidrawElementsIncludingDeleted } from "./scene/Scene";
|
||||
import { getElementLineSegments } from "./element/bounds";
|
||||
import { doLineSegmentsIntersect } from "../utils/export";
|
||||
import {
|
||||
doLineSegmentsIntersect,
|
||||
elementsOverlappingBBox,
|
||||
} from "../utils/export";
|
||||
import { isFrameElement, isFrameLikeElement } from "./element/typeChecks";
|
||||
import { ReadonlySetLike } from "./utility-types";
|
||||
|
||||
// --------------------------- Frame State ------------------------------------
|
||||
export const bindElementsToFramesAfterDuplication = (
|
||||
|
@ -104,17 +110,16 @@ export const elementsAreInFrameBounds = (
|
|||
elements: readonly ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
) => {
|
||||
const [selectionX1, selectionY1, selectionX2, selectionY2] =
|
||||
getElementAbsoluteCoords(frame);
|
||||
const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame);
|
||||
|
||||
const [elementX1, elementY1, elementX2, elementY2] =
|
||||
getCommonBounds(elements);
|
||||
|
||||
return (
|
||||
selectionX1 <= elementX1 &&
|
||||
selectionY1 <= elementY1 &&
|
||||
selectionX2 >= elementX2 &&
|
||||
selectionY2 >= elementY2
|
||||
frameX1 <= elementX1 &&
|
||||
frameY1 <= elementY1 &&
|
||||
frameX2 >= elementX2 &&
|
||||
frameY2 >= elementY2
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -209,9 +214,17 @@ export const groupByFrameLikes = (elements: readonly ExcalidrawElement[]) => {
|
|||
};
|
||||
|
||||
export const getFrameChildren = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
allElements: ElementsMapOrArray,
|
||||
frameId: string,
|
||||
) => allElements.filter((element) => element.frameId === frameId);
|
||||
) => {
|
||||
const frameChildren: ExcalidrawElement[] = [];
|
||||
for (const element of allElements.values()) {
|
||||
if (element.frameId === frameId) {
|
||||
frameChildren.push(element);
|
||||
}
|
||||
}
|
||||
return frameChildren;
|
||||
};
|
||||
|
||||
export const getFrameLikeElements = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
|
@ -369,43 +382,107 @@ export const getContainingFrame = (
|
|||
|
||||
// --------------------------- Frame Operations -------------------------------
|
||||
|
||||
/** */
|
||||
export const filterElementsEligibleAsFrameChildren = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
) => {
|
||||
const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
|
||||
|
||||
elements = omitGroupsContainingFrameLikes(elements);
|
||||
|
||||
for (const element of elements) {
|
||||
if (isFrameLikeElement(element) && element.id !== frame.id) {
|
||||
otherFrames.add(element.id);
|
||||
}
|
||||
}
|
||||
|
||||
const processedGroups = new Set<ExcalidrawElement["id"]>();
|
||||
|
||||
const eligibleElements: ExcalidrawElement[] = [];
|
||||
|
||||
for (const element of elements) {
|
||||
// don't add frames or their children
|
||||
if (
|
||||
isFrameLikeElement(element) ||
|
||||
(element.frameId && otherFrames.has(element.frameId))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (element.groupIds.length) {
|
||||
const shallowestGroupId = element.groupIds.at(-1)!;
|
||||
if (!processedGroups.has(shallowestGroupId)) {
|
||||
processedGroups.add(shallowestGroupId);
|
||||
const groupElements = getElementsInGroup(elements, shallowestGroupId);
|
||||
if (groupElements.some((el) => elementOverlapsWithFrame(el, frame))) {
|
||||
for (const child of groupElements) {
|
||||
eligibleElements.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const overlaps = elementOverlapsWithFrame(element, frame);
|
||||
if (overlaps) {
|
||||
eligibleElements.push(element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return eligibleElements;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retains (or repairs for target frame) the ordering invriant where children
|
||||
* elements come right before the parent frame:
|
||||
* [el, el, child, child, frame, el]
|
||||
*
|
||||
* @returns mutated allElements (same data structure)
|
||||
*/
|
||||
export const addElementsToFrame = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
export const addElementsToFrame = <T extends ElementsMapOrArray>(
|
||||
allElements: T,
|
||||
elementsToAdd: NonDeletedExcalidrawElement[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
) => {
|
||||
const { currTargetFrameChildrenMap } = allElements.reduce(
|
||||
(acc, element, index) => {
|
||||
if (element.frameId === frame.id) {
|
||||
acc.currTargetFrameChildrenMap.set(element.id, true);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
currTargetFrameChildrenMap: new Map<ExcalidrawElement["id"], true>(),
|
||||
},
|
||||
);
|
||||
): T => {
|
||||
const elementsMap = arrayToMap(allElements);
|
||||
const currTargetFrameChildrenMap = new Map<ExcalidrawElement["id"], true>();
|
||||
for (const element of allElements.values()) {
|
||||
if (element.frameId === frame.id) {
|
||||
currTargetFrameChildrenMap.set(element.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
const suppliedElementsToAddSet = new Set(elementsToAdd.map((el) => el.id));
|
||||
|
||||
const finalElementsToAdd: ExcalidrawElement[] = [];
|
||||
|
||||
const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
|
||||
|
||||
for (const element of elementsToAdd) {
|
||||
if (isFrameLikeElement(element) && element.id !== frame.id) {
|
||||
otherFrames.add(element.id);
|
||||
}
|
||||
}
|
||||
|
||||
// - add bound text elements if not already in the array
|
||||
// - filter out elements that are already in the frame
|
||||
for (const element of omitGroupsContainingFrameLikes(
|
||||
allElements,
|
||||
elementsToAdd,
|
||||
)) {
|
||||
// don't add frames or their children
|
||||
if (
|
||||
isFrameLikeElement(element) ||
|
||||
(element.frameId && otherFrames.has(element.frameId))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!currTargetFrameChildrenMap.has(element.id)) {
|
||||
finalElementsToAdd.push(element);
|
||||
}
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (
|
||||
boundTextElement &&
|
||||
!suppliedElementsToAddSet.has(boundTextElement.id) &&
|
||||
|
@ -424,13 +501,13 @@ export const addElementsToFrame = (
|
|||
false,
|
||||
);
|
||||
}
|
||||
return allElements.slice();
|
||||
|
||||
return allElements;
|
||||
};
|
||||
|
||||
export const removeElementsFromFrame = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
elementsToRemove: NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
elementsToRemove: ReadonlySetLike<NonDeletedExcalidrawElement>,
|
||||
elementsMap: ElementsMap,
|
||||
) => {
|
||||
const _elementsToRemove = new Map<
|
||||
ExcalidrawElement["id"],
|
||||
|
@ -449,7 +526,7 @@ export const removeElementsFromFrame = (
|
|||
const arr = toRemoveElementsByFrame.get(element.frameId) || [];
|
||||
arr.push(element);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
_elementsToRemove.set(boundTextElement.id, boundTextElement);
|
||||
arr.push(boundTextElement);
|
||||
|
@ -468,35 +545,35 @@ export const removeElementsFromFrame = (
|
|||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return allElements.slice();
|
||||
};
|
||||
|
||||
export const removeAllElementsFromFrame = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
export const removeAllElementsFromFrame = <T extends ExcalidrawElement>(
|
||||
allElements: readonly T[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
appState: AppState,
|
||||
) => {
|
||||
const elementsInFrame = getFrameChildren(allElements, frame.id);
|
||||
return removeElementsFromFrame(allElements, elementsInFrame, appState);
|
||||
removeElementsFromFrame(elementsInFrame, arrayToMap(allElements));
|
||||
return allElements;
|
||||
};
|
||||
|
||||
export const replaceAllElementsInFrame = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
export const replaceAllElementsInFrame = <T extends ExcalidrawElement>(
|
||||
allElements: readonly T[],
|
||||
nextElementsInFrame: ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
appState: AppState,
|
||||
) => {
|
||||
app: AppClassProperties,
|
||||
): T[] => {
|
||||
return addElementsToFrame(
|
||||
removeAllElementsFromFrame(allElements, frame, appState),
|
||||
removeAllElementsFromFrame(allElements, frame),
|
||||
nextElementsInFrame,
|
||||
frame,
|
||||
);
|
||||
).slice();
|
||||
};
|
||||
|
||||
/** does not mutate elements, but returns new ones */
|
||||
export const updateFrameMembershipOfSelectedElements = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
export const updateFrameMembershipOfSelectedElements = <
|
||||
T extends ElementsMapOrArray,
|
||||
>(
|
||||
allElements: T,
|
||||
appState: AppState,
|
||||
app: AppClassProperties,
|
||||
) => {
|
||||
|
@ -521,19 +598,22 @@ export const updateFrameMembershipOfSelectedElements = (
|
|||
|
||||
const elementsToRemove = new Set<ExcalidrawElement>();
|
||||
|
||||
const elementsMap = arrayToMap(allElements);
|
||||
|
||||
elementsToFilter.forEach((element) => {
|
||||
if (
|
||||
element.frameId &&
|
||||
!isFrameLikeElement(element) &&
|
||||
!isElementInFrame(element, allElements, appState)
|
||||
!isElementInFrame(element, elementsMap, appState)
|
||||
) {
|
||||
elementsToRemove.add(element);
|
||||
}
|
||||
});
|
||||
|
||||
return elementsToRemove.size > 0
|
||||
? removeElementsFromFrame(allElements, [...elementsToRemove], appState)
|
||||
: allElements;
|
||||
if (elementsToRemove.size > 0) {
|
||||
removeElementsFromFrame(elementsToRemove, elementsMap);
|
||||
}
|
||||
return allElements;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -541,14 +621,16 @@ export const updateFrameMembershipOfSelectedElements = (
|
|||
* anywhere in the group tree
|
||||
*/
|
||||
export const omitGroupsContainingFrameLikes = (
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
allElements: ElementsMapOrArray,
|
||||
/** subset of elements you want to filter. Optional perf optimization so we
|
||||
* don't have to filter all elements unnecessarily
|
||||
*/
|
||||
selectedElements?: readonly ExcalidrawElement[],
|
||||
) => {
|
||||
const uniqueGroupIds = new Set<string>();
|
||||
for (const el of selectedElements || allElements) {
|
||||
const elements = selectedElements || allElements;
|
||||
|
||||
for (const el of elements.values()) {
|
||||
const topMostGroupId = el.groupIds[el.groupIds.length - 1];
|
||||
if (topMostGroupId) {
|
||||
uniqueGroupIds.add(topMostGroupId);
|
||||
|
@ -566,9 +648,15 @@ export const omitGroupsContainingFrameLikes = (
|
|||
}
|
||||
}
|
||||
|
||||
return (selectedElements || allElements).filter(
|
||||
(el) => !rejectedGroupIds.has(el.groupIds[el.groupIds.length - 1]),
|
||||
);
|
||||
const ret: ExcalidrawElement[] = [];
|
||||
|
||||
for (const element of elements.values()) {
|
||||
if (!rejectedGroupIds.has(element.groupIds[element.groupIds.length - 1])) {
|
||||
ret.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -577,10 +665,11 @@ export const omitGroupsContainingFrameLikes = (
|
|||
*/
|
||||
export const getTargetFrame = (
|
||||
element: ExcalidrawElement,
|
||||
elementsMap: ElementsMap,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
const _element = isTextElement(element)
|
||||
? getContainerElement(element) || element
|
||||
? getContainerElement(element, elementsMap) || element
|
||||
: element;
|
||||
|
||||
return appState.selectedElementIds[_element.id] &&
|
||||
|
@ -593,12 +682,12 @@ export const getTargetFrame = (
|
|||
// given an element, return if the element is in some frame
|
||||
export const isElementInFrame = (
|
||||
element: ExcalidrawElement,
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
allElements: ElementsMap,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
const frame = getTargetFrame(element, appState);
|
||||
const frame = getTargetFrame(element, allElements, appState);
|
||||
const _element = isTextElement(element)
|
||||
? getContainerElement(element) || element
|
||||
? getContainerElement(element, allElements) || element
|
||||
: element;
|
||||
|
||||
if (frame) {
|
||||
|
@ -657,10 +746,26 @@ export const getFrameLikeTitle = (
|
|||
element: ExcalidrawFrameLikeElement,
|
||||
frameIdx: number,
|
||||
) => {
|
||||
const existingName = element.name?.trim();
|
||||
if (existingName) {
|
||||
return existingName;
|
||||
}
|
||||
// TODO name frames AI only is specific to AI frames
|
||||
return isFrameElement(element) ? `Frame ${frameIdx}` : `AI Frame ${frameIdx}`;
|
||||
// TODO name frames "AI" only if specific to AI frames
|
||||
return element.name === null
|
||||
? isFrameElement(element)
|
||||
? `Frame ${frameIdx}`
|
||||
: `AI Frame $${frameIdx}`
|
||||
: element.name;
|
||||
};
|
||||
|
||||
export const getElementsOverlappingFrame = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
frame: ExcalidrawFrameLikeElement,
|
||||
) => {
|
||||
return (
|
||||
elementsOverlappingBBox({
|
||||
elements,
|
||||
bounds: frame,
|
||||
type: "overlap",
|
||||
})
|
||||
// removes elements who are overlapping, but are in a different frame,
|
||||
// and thus invisible in target frame
|
||||
.filter((el) => !el.frameId || el.frameId === frame.id)
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,6 +3,8 @@ import {
|
|||
ExcalidrawElement,
|
||||
NonDeleted,
|
||||
NonDeletedExcalidrawElement,
|
||||
ElementsMapOrArray,
|
||||
ElementsMap,
|
||||
} from "./element/types";
|
||||
import {
|
||||
AppClassProperties,
|
||||
|
@ -270,9 +272,17 @@ export const isElementInGroup = (element: ExcalidrawElement, groupId: string) =>
|
|||
element.groupIds.includes(groupId);
|
||||
|
||||
export const getElementsInGroup = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: ElementsMapOrArray,
|
||||
groupId: string,
|
||||
) => elements.filter((element) => isElementInGroup(element, groupId));
|
||||
) => {
|
||||
const elementsInGroup: ExcalidrawElement[] = [];
|
||||
for (const element of elements.values()) {
|
||||
if (isElementInGroup(element, groupId)) {
|
||||
elementsInGroup.push(element);
|
||||
}
|
||||
}
|
||||
return elementsInGroup;
|
||||
};
|
||||
|
||||
export const getSelectedGroupIdForElement = (
|
||||
element: ExcalidrawElement,
|
||||
|
@ -320,12 +330,12 @@ export const removeFromSelectedGroups = (
|
|||
|
||||
export const getMaximumGroups = (
|
||||
elements: ExcalidrawElement[],
|
||||
elementsMap: ElementsMap,
|
||||
): ExcalidrawElement[][] => {
|
||||
const groups: Map<String, ExcalidrawElement[]> = new Map<
|
||||
String,
|
||||
ExcalidrawElement[]
|
||||
>();
|
||||
|
||||
elements.forEach((element: ExcalidrawElement) => {
|
||||
const groupId =
|
||||
element.groupIds.length === 0
|
||||
|
@ -335,7 +345,7 @@ export const getMaximumGroups = (
|
|||
const currentGroupMembers = groups.get(groupId) || [];
|
||||
|
||||
// Include bound text if present when grouping
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
if (boundTextElement) {
|
||||
currentGroupMembers.push(boundTextElement);
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
generateIdForFile,
|
||||
onLinkOpen,
|
||||
onPointerDown,
|
||||
onPointerUp,
|
||||
onScrollChange,
|
||||
children,
|
||||
scrollConstraints,
|
||||
|
@ -81,6 +82,13 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
const importPolyfill = async () => {
|
||||
//@ts-ignore
|
||||
await import("canvas-roundrect-polyfill");
|
||||
};
|
||||
|
||||
importPolyfill();
|
||||
|
||||
// Block pinch-zooming on iOS outside of the content area
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
// @ts-ignore
|
||||
|
@ -125,6 +133,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||
generateIdForFile={generateIdForFile}
|
||||
onLinkOpen={onLinkOpen}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
onScrollChange={onScrollChange}
|
||||
scrollConstraints={scrollConstraints}
|
||||
validateEmbeddable={validateEmbeddable}
|
||||
|
@ -225,7 +234,7 @@ export {
|
|||
} from "../utils/export";
|
||||
export { isLinearElement } from "./element/typeChecks";
|
||||
|
||||
export { FONT_FAMILY, THEME, MIME_TYPES } from "./constants";
|
||||
export { FONT_FAMILY, THEME, MIME_TYPES, ROUNDNESS } from "./constants";
|
||||
|
||||
export {
|
||||
mutateElement,
|
||||
|
|
|
@ -301,9 +301,12 @@
|
|||
"openIssueMessage": "We were very cautious not to include your scene information on the error. If your scene is not private, please consider following up on our <button>bug tracker</button>. Please include information below by copying and pasting into the GitHub issue.",
|
||||
"sceneContent": "Scene content:"
|
||||
},
|
||||
"shareDialog": {
|
||||
"or": "Or"
|
||||
},
|
||||
"roomDialog": {
|
||||
"desc_intro": "You can invite people to your current scene to collaborate with you.",
|
||||
"desc_privacy": "Don't worry, the session uses end-to-end encryption, so whatever you draw will stay private. Not even our server will be able to see what you come up with.",
|
||||
"desc_intro": "Invite people to collaborate on your drawing.",
|
||||
"desc_privacy": "Don't worry, the session is end-to-end encrypted, and fully private. Not even our server can see what you draw.",
|
||||
"button_startSession": "Start session",
|
||||
"button_stopSession": "Stop session",
|
||||
"desc_inProgressIntro": "Live-collaboration session is now in progress.",
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
"exports": {
|
||||
".": {
|
||||
"development": "./dist/dev/index.js",
|
||||
"default": "./dist/prod/index.js",
|
||||
"types": "./dist/excalidraw/index.d.ts"
|
||||
"types": "./dist/excalidraw/index.d.ts",
|
||||
"default": "./dist/prod/index.js"
|
||||
},
|
||||
"./index.css": {
|
||||
"development": "./dist/dev/index.css",
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
ExcalidrawImageElement,
|
||||
ExcalidrawTextElementWithContainer,
|
||||
ExcalidrawFrameLikeElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import {
|
||||
isTextElement,
|
||||
|
@ -21,7 +22,11 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
|||
import type { Drawable } from "roughjs/bin/core";
|
||||
import type { RoughSVG } from "roughjs/bin/svg";
|
||||
|
||||
import { SVGRenderConfig, StaticCanvasRenderConfig } from "../scene/types";
|
||||
import {
|
||||
SVGRenderConfig,
|
||||
StaticCanvasRenderConfig,
|
||||
RenderableElementsMap,
|
||||
} from "../scene/types";
|
||||
import {
|
||||
distance,
|
||||
getFontString,
|
||||
|
@ -186,6 +191,7 @@ const cappedElementCanvasSize = (
|
|||
|
||||
const generateElementCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: RenderableElementsMap,
|
||||
zoom: Zoom,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
|
@ -243,7 +249,8 @@ const generateElementCanvas = (
|
|||
zoomValue: zoom.value,
|
||||
canvasOffsetX,
|
||||
canvasOffsetY,
|
||||
boundTextElementVersion: getBoundTextElement(element)?.version || null,
|
||||
boundTextElementVersion:
|
||||
getBoundTextElement(element, elementsMap)?.version || null,
|
||||
containingFrameOpacity: getContainingFrame(element)?.opacity || 100,
|
||||
};
|
||||
};
|
||||
|
@ -337,6 +344,17 @@ const drawElementOnCanvas = (
|
|||
? renderConfig.imageCache.get(element.fileId)?.image
|
||||
: undefined;
|
||||
if (img != null && !(img instanceof Promise)) {
|
||||
if (element.roundness && context.roundRect) {
|
||||
context.beginPath();
|
||||
context.roundRect(
|
||||
0,
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
getCornerRadius(Math.min(element.width, element.height), element),
|
||||
);
|
||||
context.clip();
|
||||
}
|
||||
context.drawImage(
|
||||
img,
|
||||
0 /* hardcoded for the selection box*/,
|
||||
|
@ -403,6 +421,7 @@ export const elementWithCanvasCache = new WeakMap<
|
|||
|
||||
const generateElementWithCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: RenderableElementsMap,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
|
@ -412,7 +431,9 @@ const generateElementWithCanvas = (
|
|||
prevElementWithCanvas &&
|
||||
prevElementWithCanvas.zoomValue !== zoom.value &&
|
||||
!appState?.shouldCacheIgnoreZoom;
|
||||
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
||||
const boundTextElementVersion =
|
||||
getBoundTextElement(element, elementsMap)?.version || null;
|
||||
|
||||
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
|
||||
|
||||
if (
|
||||
|
@ -424,6 +445,7 @@ const generateElementWithCanvas = (
|
|||
) {
|
||||
const elementWithCanvas = generateElementCanvas(
|
||||
element,
|
||||
elementsMap,
|
||||
zoom,
|
||||
renderConfig,
|
||||
appState,
|
||||
|
@ -441,6 +463,7 @@ const drawElementFromCanvas = (
|
|||
context: CanvasRenderingContext2D,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
allElementsMap: NonDeletedSceneElementsMap,
|
||||
) => {
|
||||
const element = elementWithCanvas.element;
|
||||
const padding = getCanvasPadding(element);
|
||||
|
@ -460,7 +483,8 @@ const drawElementFromCanvas = (
|
|||
|
||||
context.save();
|
||||
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
|
||||
const boundTextElement = getBoundTextElement(element, allElementsMap);
|
||||
|
||||
if (isArrowElement(element) && boundTextElement) {
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
|
@ -507,7 +531,6 @@ const drawElementFromCanvas = (
|
|||
offsetY -
|
||||
padding * zoom;
|
||||
tempCanvasContext.translate(-shiftX, -shiftY);
|
||||
|
||||
// Clear the bound text area
|
||||
tempCanvasContext.clearRect(
|
||||
-(boundTextElement.width / 2 + BOUND_TEXT_PADDING) *
|
||||
|
@ -569,6 +592,7 @@ const drawElementFromCanvas = (
|
|||
) {
|
||||
const textElement = getBoundTextElement(
|
||||
element,
|
||||
allElementsMap,
|
||||
) as ExcalidrawTextElementWithContainer;
|
||||
const coords = getContainerCoords(element);
|
||||
context.strokeStyle = "#c92a2a";
|
||||
|
@ -576,7 +600,7 @@ const drawElementFromCanvas = (
|
|||
context.strokeRect(
|
||||
(coords.x + appState.scrollX) * window.devicePixelRatio,
|
||||
(coords.y + appState.scrollY) * window.devicePixelRatio,
|
||||
getBoundTextMaxWidth(element) * window.devicePixelRatio,
|
||||
getBoundTextMaxWidth(element, textElement) * window.devicePixelRatio,
|
||||
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
|
||||
);
|
||||
}
|
||||
|
@ -611,6 +635,8 @@ export const renderSelectionElement = (
|
|||
|
||||
export const renderElement = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: RenderableElementsMap,
|
||||
allElementsMap: NonDeletedSceneElementsMap,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
|
@ -682,6 +708,7 @@ export const renderElement = (
|
|||
} else {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
element,
|
||||
elementsMap,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
|
@ -690,6 +717,7 @@ export const renderElement = (
|
|||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
allElementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -715,7 +743,7 @@ export const renderElement = (
|
|||
let shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||
let shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||
if (isTextElement(element)) {
|
||||
const container = getContainerElement(element);
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (isArrowElement(container)) {
|
||||
const boundTextCoords =
|
||||
LinearElementEditor.getBoundTextElementPosition(
|
||||
|
@ -732,7 +760,7 @@ export const renderElement = (
|
|||
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||
context.filter = "none";
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
const boundTextElement = getBoundTextElement(element, elementsMap);
|
||||
|
||||
if (isArrowElement(element) && boundTextElement) {
|
||||
const tempCanvas = document.createElement("canvas");
|
||||
|
@ -815,6 +843,7 @@ export const renderElement = (
|
|||
} else {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
element,
|
||||
elementsMap,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
|
@ -846,6 +875,7 @@ export const renderElement = (
|
|||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
allElementsMap,
|
||||
);
|
||||
|
||||
// reset
|
||||
|
@ -900,6 +930,7 @@ const maybeWrapNodesInFrameClipPath = (
|
|||
|
||||
export const renderElementToSvg = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
elementsMap: RenderableElementsMap,
|
||||
rsvg: RoughSVG,
|
||||
svgRoot: SVGElement,
|
||||
files: BinaryFiles,
|
||||
|
@ -912,7 +943,7 @@ export const renderElementToSvg = (
|
|||
let cx = (x2 - x1) / 2 - (element.x - x1);
|
||||
let cy = (y2 - y1) / 2 - (element.y - y1);
|
||||
if (isTextElement(element)) {
|
||||
const container = getContainerElement(element);
|
||||
const container = getContainerElement(element, elementsMap);
|
||||
if (isArrowElement(container)) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(container);
|
||||
|
||||
|
@ -1013,6 +1044,7 @@ export const renderElementToSvg = (
|
|||
createPlaceholderEmbeddableLabel(element);
|
||||
renderElementToSvg(
|
||||
label,
|
||||
elementsMap,
|
||||
rsvg,
|
||||
root,
|
||||
files,
|
||||
|
@ -1089,7 +1121,7 @@ export const renderElementToSvg = (
|
|||
}
|
||||
case "line":
|
||||
case "arrow": {
|
||||
const boundText = getBoundTextElement(element);
|
||||
const boundText = getBoundTextElement(element, elementsMap);
|
||||
const maskPath = svgRoot.ownerDocument!.createElementNS(SVG_NS, "mask");
|
||||
if (boundText) {
|
||||
maskPath.setAttribute("id", `mask-${element.id}`);
|
||||
|
@ -1280,6 +1312,31 @@ export const renderElementToSvg = (
|
|||
}) rotate(${degree} ${cx} ${cy})`,
|
||||
);
|
||||
|
||||
if (element.roundness) {
|
||||
const clipPath = svgRoot.ownerDocument!.createElementNS(
|
||||
SVG_NS,
|
||||
"clipPath",
|
||||
);
|
||||
clipPath.id = `image-clipPath-${element.id}`;
|
||||
|
||||
const clipRect = svgRoot.ownerDocument!.createElementNS(
|
||||
SVG_NS,
|
||||
"rect",
|
||||
);
|
||||
const radius = getCornerRadius(
|
||||
Math.min(element.width, element.height),
|
||||
element,
|
||||
);
|
||||
clipRect.setAttribute("width", `${element.width}`);
|
||||
clipRect.setAttribute("height", `${element.height}`);
|
||||
clipRect.setAttribute("rx", `${radius}`);
|
||||
clipRect.setAttribute("ry", `${radius}`);
|
||||
clipPath.appendChild(clipRect);
|
||||
addToRoot(clipPath, element);
|
||||
|
||||
g.setAttributeNS(SVG_NS, "clip-path", `url(#${clipPath.id})`);
|
||||
}
|
||||
|
||||
const clipG = maybeWrapNodesInFrameClipPath(
|
||||
element,
|
||||
root,
|
||||
|
|
|
@ -33,6 +33,7 @@ import {
|
|||
SVGRenderConfig,
|
||||
StaticCanvasRenderConfig,
|
||||
StaticSceneRenderConfig,
|
||||
RenderableElementsMap,
|
||||
} from "../scene/types";
|
||||
import {
|
||||
getScrollBars,
|
||||
|
@ -61,9 +62,13 @@ import {
|
|||
TransformHandles,
|
||||
TransformHandleType,
|
||||
} from "../element/transformHandles";
|
||||
import { throttleRAF } from "../utils";
|
||||
import { arrayToMap, throttleRAF } from "../utils";
|
||||
import { UserIdleState } from "../types";
|
||||
import { FRAME_STYLE, THEME_FILTER } from "../constants";
|
||||
import {
|
||||
DEFAULT_TRANSFORM_HANDLE_SPACING,
|
||||
FRAME_STYLE,
|
||||
THEME_FILTER,
|
||||
} from "../constants";
|
||||
import {
|
||||
EXTERNAL_LINK_IMG,
|
||||
getLinkHandleFromCoords,
|
||||
|
@ -75,18 +80,12 @@ import {
|
|||
isIframeLikeElement,
|
||||
isLinearElement,
|
||||
} from "../element/typeChecks";
|
||||
import {
|
||||
isIframeLikeOrItsLabel,
|
||||
createPlaceholderEmbeddableLabel,
|
||||
} from "../element/embeddable";
|
||||
import { createPlaceholderEmbeddableLabel } from "../element/embeddable";
|
||||
import {
|
||||
elementOverlapsWithFrame,
|
||||
getTargetFrame,
|
||||
isElementInFrame,
|
||||
} from "../frame";
|
||||
import "canvas-roundrect-polyfill";
|
||||
|
||||
export const DEFAULT_SPACING = 2;
|
||||
|
||||
const strokeRectWithRotation = (
|
||||
context: CanvasRenderingContext2D,
|
||||
|
@ -249,6 +248,7 @@ const renderLinearPointHandles = (
|
|||
context: CanvasRenderingContext2D,
|
||||
appState: InteractiveCanvasAppState,
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap: RenderableElementsMap,
|
||||
) => {
|
||||
if (!appState.selectedLinearElement) {
|
||||
return;
|
||||
|
@ -272,6 +272,7 @@ const renderLinearPointHandles = (
|
|||
//Rendering segment mid points
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
element,
|
||||
elementsMap,
|
||||
appState,
|
||||
).filter((midPoint) => midPoint !== null) as Point[];
|
||||
|
||||
|
@ -446,7 +447,7 @@ const bootstrapCanvas = ({
|
|||
|
||||
const _renderInteractiveScene = ({
|
||||
canvas,
|
||||
elements,
|
||||
elementsMap,
|
||||
visibleElements,
|
||||
selectedElements,
|
||||
scale,
|
||||
|
@ -454,7 +455,7 @@ const _renderInteractiveScene = ({
|
|||
renderConfig,
|
||||
}: InteractiveSceneRenderConfig) => {
|
||||
if (canvas === null) {
|
||||
return { atLeastOneVisibleElement: false, elements };
|
||||
return { atLeastOneVisibleElement: false, elementsMap };
|
||||
}
|
||||
|
||||
const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions(
|
||||
|
@ -488,7 +489,12 @@ const _renderInteractiveScene = ({
|
|||
});
|
||||
|
||||
if (editingLinearElement) {
|
||||
renderLinearPointHandles(context, appState, editingLinearElement);
|
||||
renderLinearPointHandles(
|
||||
context,
|
||||
appState,
|
||||
editingLinearElement,
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
// Paint selection element
|
||||
|
@ -531,6 +537,7 @@ const _renderInteractiveScene = ({
|
|||
context,
|
||||
appState,
|
||||
selectedElements[0] as NonDeleted<ExcalidrawLinearElement>,
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -556,81 +563,71 @@ const _renderInteractiveScene = ({
|
|||
context,
|
||||
appState,
|
||||
selectedElements[0] as ExcalidrawLinearElement,
|
||||
elementsMap,
|
||||
);
|
||||
}
|
||||
const selectionColor = renderConfig.selectionColor || oc.black;
|
||||
|
||||
if (showBoundingBox) {
|
||||
// Optimisation for finding quickly relevant element ids
|
||||
const locallySelectedIds = selectedElements.reduce(
|
||||
(acc: Record<string, boolean>, element) => {
|
||||
acc[element.id] = true;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const locallySelectedIds = arrayToMap(selectedElements);
|
||||
|
||||
const selections = elements.reduce(
|
||||
(
|
||||
acc: {
|
||||
angle: number;
|
||||
elementX1: number;
|
||||
elementY1: number;
|
||||
elementX2: number;
|
||||
elementY2: number;
|
||||
selectionColors: string[];
|
||||
dashed?: boolean;
|
||||
cx: number;
|
||||
cy: number;
|
||||
activeEmbeddable: boolean;
|
||||
}[],
|
||||
element,
|
||||
) => {
|
||||
const selectionColors = [];
|
||||
// local user
|
||||
if (
|
||||
locallySelectedIds[element.id] &&
|
||||
!isSelectedViaGroup(appState, element)
|
||||
) {
|
||||
selectionColors.push(selectionColor);
|
||||
}
|
||||
// remote users
|
||||
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
||||
selectionColors.push(
|
||||
...renderConfig.remoteSelectedElementIds[element.id].map(
|
||||
(socketId: string) => {
|
||||
const background = getClientColor(socketId);
|
||||
return background;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
const selections: {
|
||||
angle: number;
|
||||
elementX1: number;
|
||||
elementY1: number;
|
||||
elementX2: number;
|
||||
elementY2: number;
|
||||
selectionColors: string[];
|
||||
dashed?: boolean;
|
||||
cx: number;
|
||||
cy: number;
|
||||
activeEmbeddable: boolean;
|
||||
}[] = [];
|
||||
|
||||
if (selectionColors.length) {
|
||||
const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
|
||||
getElementAbsoluteCoords(element, true);
|
||||
acc.push({
|
||||
angle: element.angle,
|
||||
elementX1,
|
||||
elementY1,
|
||||
elementX2,
|
||||
elementY2,
|
||||
selectionColors,
|
||||
dashed: !!renderConfig.remoteSelectedElementIds[element.id],
|
||||
cx,
|
||||
cy,
|
||||
activeEmbeddable:
|
||||
appState.activeEmbeddable?.element === element &&
|
||||
appState.activeEmbeddable.state === "active",
|
||||
});
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
for (const element of elementsMap.values()) {
|
||||
const selectionColors = [];
|
||||
// local user
|
||||
if (
|
||||
locallySelectedIds.has(element.id) &&
|
||||
!isSelectedViaGroup(appState, element)
|
||||
) {
|
||||
selectionColors.push(selectionColor);
|
||||
}
|
||||
// remote users
|
||||
if (renderConfig.remoteSelectedElementIds[element.id]) {
|
||||
selectionColors.push(
|
||||
...renderConfig.remoteSelectedElementIds[element.id].map(
|
||||
(socketId: string) => {
|
||||
const background = getClientColor(socketId);
|
||||
return background;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (selectionColors.length) {
|
||||
const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
|
||||
getElementAbsoluteCoords(element, true);
|
||||
selections.push({
|
||||
angle: element.angle,
|
||||
elementX1,
|
||||
elementY1,
|
||||
elementX2,
|
||||
elementY2,
|
||||
selectionColors,
|
||||
dashed: !!renderConfig.remoteSelectedElementIds[element.id],
|
||||
cx,
|
||||
cy,
|
||||
activeEmbeddable:
|
||||
appState.activeEmbeddable?.element === element &&
|
||||
appState.activeEmbeddable.state === "active",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const addSelectionForGroupId = (groupId: GroupId) => {
|
||||
const groupElements = getElementsInGroup(elements, groupId);
|
||||
const groupElements = getElementsInGroup(elementsMap, groupId);
|
||||
const [elementX1, elementY1, elementX2, elementY2] =
|
||||
getCommonBounds(groupElements);
|
||||
selections.push({
|
||||
|
@ -681,7 +678,8 @@ const _renderInteractiveScene = ({
|
|||
);
|
||||
}
|
||||
} else if (selectedElements.length > 1 && !appState.isRotating) {
|
||||
const dashedLinePadding = (DEFAULT_SPACING * 2) / appState.zoom.value;
|
||||
const dashedLinePadding =
|
||||
(DEFAULT_TRANSFORM_HANDLE_SPACING * 2) / appState.zoom.value;
|
||||
context.fillStyle = oc.white;
|
||||
const [x1, y1, x2, y2] = getCommonBounds(selectedElements);
|
||||
const initialLineDash = context.getLineDash();
|
||||
|
@ -870,7 +868,7 @@ const _renderInteractiveScene = ({
|
|||
let scrollBars;
|
||||
if (renderConfig.renderScrollbars) {
|
||||
scrollBars = getScrollBars(
|
||||
elements,
|
||||
elementsMap,
|
||||
normalizedWidth,
|
||||
normalizedHeight,
|
||||
appState,
|
||||
|
@ -897,14 +895,15 @@ const _renderInteractiveScene = ({
|
|||
return {
|
||||
scrollBars,
|
||||
atLeastOneVisibleElement: visibleElements.length > 0,
|
||||
elements,
|
||||
elementsMap,
|
||||
};
|
||||
};
|
||||
|
||||
const _renderStaticScene = ({
|
||||
canvas,
|
||||
rc,
|
||||
elements,
|
||||
elementsMap,
|
||||
allElementsMap,
|
||||
visibleElements,
|
||||
scale,
|
||||
appState,
|
||||
|
@ -965,7 +964,7 @@ const _renderStaticScene = ({
|
|||
|
||||
// Paint visible elements
|
||||
visibleElements
|
||||
.filter((el) => !isIframeLikeOrItsLabel(el))
|
||||
.filter((el) => !isIframeLikeElement(el))
|
||||
.forEach((element) => {
|
||||
try {
|
||||
const frameId = element.frameId || appState.frameToHighlight?.id;
|
||||
|
@ -977,16 +976,32 @@ const _renderStaticScene = ({
|
|||
) {
|
||||
context.save();
|
||||
|
||||
const frame = getTargetFrame(element, appState);
|
||||
const frame = getTargetFrame(element, elementsMap, appState);
|
||||
|
||||
// TODO do we need to check isElementInFrame here?
|
||||
if (frame && isElementInFrame(element, elements, appState)) {
|
||||
if (frame && isElementInFrame(element, elementsMap, appState)) {
|
||||
frameClip(frame, context, renderConfig, appState);
|
||||
}
|
||||
renderElement(element, rc, context, renderConfig, appState);
|
||||
renderElement(
|
||||
element,
|
||||
elementsMap,
|
||||
allElementsMap,
|
||||
rc,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
context.restore();
|
||||
} else {
|
||||
renderElement(element, rc, context, renderConfig, appState);
|
||||
renderElement(
|
||||
element,
|
||||
elementsMap,
|
||||
allElementsMap,
|
||||
rc,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
}
|
||||
if (!isExporting) {
|
||||
renderLinkIcon(element, context, appState);
|
||||
|
@ -998,11 +1013,19 @@ const _renderStaticScene = ({
|
|||
|
||||
// render embeddables on top
|
||||
visibleElements
|
||||
.filter((el) => isIframeLikeOrItsLabel(el))
|
||||
.filter((el) => isIframeLikeElement(el))
|
||||
.forEach((element) => {
|
||||
try {
|
||||
const render = () => {
|
||||
renderElement(element, rc, context, renderConfig, appState);
|
||||
renderElement(
|
||||
element,
|
||||
elementsMap,
|
||||
allElementsMap,
|
||||
rc,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
|
||||
if (
|
||||
isIframeLikeElement(element) &&
|
||||
|
@ -1014,7 +1037,15 @@ const _renderStaticScene = ({
|
|||
element.height
|
||||
) {
|
||||
const label = createPlaceholderEmbeddableLabel(element);
|
||||
renderElement(label, rc, context, renderConfig, appState);
|
||||
renderElement(
|
||||
label,
|
||||
elementsMap,
|
||||
allElementsMap,
|
||||
rc,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
}
|
||||
if (!isExporting) {
|
||||
renderLinkIcon(element, context, appState);
|
||||
|
@ -1032,9 +1063,9 @@ const _renderStaticScene = ({
|
|||
) {
|
||||
context.save();
|
||||
|
||||
const frame = getTargetFrame(element, appState);
|
||||
const frame = getTargetFrame(element, elementsMap, appState);
|
||||
|
||||
if (frame && isElementInFrame(element, elements, appState)) {
|
||||
if (frame && isElementInFrame(element, elementsMap, appState)) {
|
||||
frameClip(frame, context, renderConfig, appState);
|
||||
}
|
||||
render();
|
||||
|
@ -1163,7 +1194,7 @@ const renderSelectionBorder = (
|
|||
cy: number;
|
||||
activeEmbeddable: boolean;
|
||||
},
|
||||
padding = DEFAULT_SPACING * 2,
|
||||
padding = DEFAULT_TRANSFORM_HANDLE_SPACING * 2,
|
||||
) => {
|
||||
const {
|
||||
angle,
|
||||
|
@ -1448,6 +1479,7 @@ const renderLinkIcon = (
|
|||
// This should be only called for exporting purposes
|
||||
export const renderSceneToSvg = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elementsMap: RenderableElementsMap,
|
||||
rsvg: RoughSVG,
|
||||
svgRoot: SVGElement,
|
||||
files: BinaryFiles,
|
||||
|
@ -1459,12 +1491,13 @@ export const renderSceneToSvg = (
|
|||
|
||||
// render elements
|
||||
elements
|
||||
.filter((el) => !isIframeLikeOrItsLabel(el))
|
||||
.filter((el) => !isIframeLikeElement(el))
|
||||
.forEach((element) => {
|
||||
if (!element.isDeleted) {
|
||||
try {
|
||||
renderElementToSvg(
|
||||
element,
|
||||
elementsMap,
|
||||
rsvg,
|
||||
svgRoot,
|
||||
files,
|
||||
|
@ -1486,6 +1519,7 @@ export const renderSceneToSvg = (
|
|||
try {
|
||||
renderElementToSvg(
|
||||
element,
|
||||
elementsMap,
|
||||
rsvg,
|
||||
svgRoot,
|
||||
files,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { isTextElement, refreshTextDimensions } from "../element";
|
||||
import { newElementWith } from "../element/mutateElement";
|
||||
import { getContainerElement } from "../element/textElement";
|
||||
import { isBoundToContainer } from "../element/typeChecks";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||
import { getFontString } from "../utils";
|
||||
|
@ -57,7 +58,13 @@ export class Fonts {
|
|||
ShapeCache.delete(element);
|
||||
didUpdate = true;
|
||||
return newElementWith(element, {
|
||||
...refreshTextDimensions(element),
|
||||
...refreshTextDimensions(
|
||||
element,
|
||||
getContainerElement(
|
||||
element,
|
||||
this.scene.getElementsMapIncludingDeleted(),
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
return element;
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
import { isElementInViewport } from "../element/sizeHelpers";
|
||||
import { isImageElement } from "../element/typeChecks";
|
||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import {
|
||||
NonDeletedElementsMap,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { cancelRender } from "../renderer/renderScene";
|
||||
import { AppState } from "../types";
|
||||
import { memoize } from "../utils";
|
||||
import { memoize, toBrandedType } from "../utils";
|
||||
import Scene from "./Scene";
|
||||
import { RenderableElementsMap } from "./types";
|
||||
|
||||
export class Renderer {
|
||||
private scene: Scene;
|
||||
|
@ -15,7 +19,7 @@ export class Renderer {
|
|||
|
||||
public getRenderableElements = (() => {
|
||||
const getVisibleCanvasElements = ({
|
||||
elements,
|
||||
elementsMap,
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
|
@ -24,7 +28,7 @@ export class Renderer {
|
|||
height,
|
||||
width,
|
||||
}: {
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
elementsMap: NonDeletedElementsMap;
|
||||
zoom: AppState["zoom"];
|
||||
offsetLeft: AppState["offsetLeft"];
|
||||
offsetTop: AppState["offsetTop"];
|
||||
|
@ -33,43 +37,55 @@ export class Renderer {
|
|||
height: AppState["height"];
|
||||
width: AppState["width"];
|
||||
}): readonly NonDeletedExcalidrawElement[] => {
|
||||
return elements.filter((element) =>
|
||||
isElementInViewport(element, width, height, {
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
}),
|
||||
);
|
||||
const visibleElements: NonDeletedExcalidrawElement[] = [];
|
||||
for (const element of elementsMap.values()) {
|
||||
if (
|
||||
isElementInViewport(element, width, height, {
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
scrollX,
|
||||
scrollY,
|
||||
})
|
||||
) {
|
||||
visibleElements.push(element);
|
||||
}
|
||||
}
|
||||
return visibleElements;
|
||||
};
|
||||
|
||||
const getCanvasElements = ({
|
||||
editingElement,
|
||||
const getRenderableElements = ({
|
||||
elements,
|
||||
editingElement,
|
||||
pendingImageElementId,
|
||||
}: {
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
editingElement: AppState["editingElement"];
|
||||
pendingImageElementId: AppState["pendingImageElementId"];
|
||||
}) => {
|
||||
return elements.filter((element) => {
|
||||
const elementsMap = toBrandedType<RenderableElementsMap>(new Map());
|
||||
|
||||
for (const element of elements) {
|
||||
if (isImageElement(element)) {
|
||||
if (
|
||||
// => not placed on canvas yet (but in elements array)
|
||||
pendingImageElementId === element.id
|
||||
) {
|
||||
return false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// we don't want to render text element that's being currently edited
|
||||
// (it's rendered on remote only)
|
||||
return (
|
||||
if (
|
||||
!editingElement ||
|
||||
editingElement.type !== "text" ||
|
||||
element.id !== editingElement.id
|
||||
);
|
||||
});
|
||||
) {
|
||||
elementsMap.set(element.id, element);
|
||||
}
|
||||
}
|
||||
return elementsMap;
|
||||
};
|
||||
|
||||
return memoize(
|
||||
|
@ -100,14 +116,14 @@ export class Renderer {
|
|||
}) => {
|
||||
const elements = this.scene.getNonDeletedElements();
|
||||
|
||||
const canvasElements = getCanvasElements({
|
||||
const elementsMap = getRenderableElements({
|
||||
elements,
|
||||
editingElement,
|
||||
pendingImageElementId,
|
||||
});
|
||||
|
||||
const visibleElements = getVisibleCanvasElements({
|
||||
elements: canvasElements,
|
||||
elementsMap,
|
||||
zoom,
|
||||
offsetLeft,
|
||||
offsetTop,
|
||||
|
@ -117,7 +133,7 @@ export class Renderer {
|
|||
width,
|
||||
});
|
||||
|
||||
return { canvasElements, visibleElements };
|
||||
return { elementsMap, visibleElements };
|
||||
},
|
||||
);
|
||||
})();
|
||||
|
|
|
@ -3,14 +3,18 @@ import {
|
|||
NonDeletedExcalidrawElement,
|
||||
NonDeleted,
|
||||
ExcalidrawFrameLikeElement,
|
||||
ElementsMapOrArray,
|
||||
SceneElementsMap,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import { getNonDeletedElements, isNonDeletedElement } from "../element";
|
||||
import { isNonDeletedElement } from "../element";
|
||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||
import { isFrameLikeElement } from "../element/typeChecks";
|
||||
import { getSelectedElements } from "./selection";
|
||||
import { AppState } from "../types";
|
||||
import { Assert, SameType } from "../utility-types";
|
||||
import { randomInteger } from "../random";
|
||||
import { toBrandedType } from "../utils";
|
||||
|
||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
|
@ -20,6 +24,20 @@ type SceneStateCallbackRemover = () => void;
|
|||
|
||||
type SelectionHash = string & { __brand: "selectionHash" };
|
||||
|
||||
const getNonDeletedElements = <T extends ExcalidrawElement>(
|
||||
allElements: readonly T[],
|
||||
) => {
|
||||
const elementsMap = new Map() as NonDeletedSceneElementsMap;
|
||||
const elements: T[] = [];
|
||||
for (const element of allElements) {
|
||||
if (!element.isDeleted) {
|
||||
elements.push(element as NonDeleted<T>);
|
||||
elementsMap.set(element.id, element as NonDeletedExcalidrawElement);
|
||||
}
|
||||
}
|
||||
return { elementsMap, elements };
|
||||
};
|
||||
|
||||
const hashSelectionOpts = (
|
||||
opts: Parameters<InstanceType<typeof Scene>["getSelectedElements"]>[0],
|
||||
) => {
|
||||
|
@ -102,11 +120,14 @@ class Scene {
|
|||
private callbacks: Set<SceneStateCallback> = new Set();
|
||||
|
||||
private nonDeletedElements: readonly NonDeletedExcalidrawElement[] = [];
|
||||
private nonDeletedElementsMap = toBrandedType<NonDeletedSceneElementsMap>(
|
||||
new Map(),
|
||||
);
|
||||
private elements: readonly ExcalidrawElement[] = [];
|
||||
private nonDeletedFramesLikes: readonly NonDeleted<ExcalidrawFrameLikeElement>[] =
|
||||
[];
|
||||
private frames: readonly ExcalidrawFrameLikeElement[] = [];
|
||||
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
|
||||
private elementsMap = toBrandedType<SceneElementsMap>(new Map());
|
||||
private selectedElementsCache: {
|
||||
selectedElementIds: AppState["selectedElementIds"] | null;
|
||||
elements: readonly NonDeletedExcalidrawElement[] | null;
|
||||
|
@ -118,6 +139,14 @@ class Scene {
|
|||
};
|
||||
private versionNonce: number | undefined;
|
||||
|
||||
getElementsMapIncludingDeleted() {
|
||||
return this.elementsMap;
|
||||
}
|
||||
|
||||
getNonDeletedElementsMap() {
|
||||
return this.nonDeletedElementsMap;
|
||||
}
|
||||
|
||||
getElementsIncludingDeleted() {
|
||||
return this.elements;
|
||||
}
|
||||
|
@ -138,7 +167,7 @@ class Scene {
|
|||
* scene state. This in effect will likely result in cache-miss, and
|
||||
* the cache won't be updated in this case.
|
||||
*/
|
||||
elements?: readonly ExcalidrawElement[];
|
||||
elements?: ElementsMapOrArray;
|
||||
// selection-related options
|
||||
includeBoundTextElement?: boolean;
|
||||
includeElementsInFrames?: boolean;
|
||||
|
@ -227,23 +256,27 @@ class Scene {
|
|||
return didChange;
|
||||
}
|
||||
|
||||
replaceAllElements(
|
||||
nextElements: readonly ExcalidrawElement[],
|
||||
mapElementIds = true,
|
||||
) {
|
||||
this.elements = nextElements;
|
||||
replaceAllElements(nextElements: ElementsMapOrArray, mapElementIds = true) {
|
||||
this.elements =
|
||||
// ts doesn't like `Array.isArray` of `instanceof Map`
|
||||
nextElements instanceof Array
|
||||
? nextElements
|
||||
: Array.from(nextElements.values());
|
||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||
this.elementsMap.clear();
|
||||
nextElements.forEach((element) => {
|
||||
this.elements.forEach((element) => {
|
||||
if (isFrameLikeElement(element)) {
|
||||
nextFrameLikes.push(element);
|
||||
}
|
||||
this.elementsMap.set(element.id, element);
|
||||
Scene.mapElementToScene(element, this);
|
||||
Scene.mapElementToScene(element, this, mapElementIds);
|
||||
});
|
||||
this.nonDeletedElements = getNonDeletedElements(this.elements);
|
||||
const nonDeletedElements = getNonDeletedElements(this.elements);
|
||||
this.nonDeletedElements = nonDeletedElements.elements;
|
||||
this.nonDeletedElementsMap = nonDeletedElements.elementsMap;
|
||||
|
||||
this.frames = nextFrameLikes;
|
||||
this.nonDeletedFramesLikes = getNonDeletedElements(this.frames);
|
||||
this.nonDeletedFramesLikes = getNonDeletedElements(this.frames).elements;
|
||||
|
||||
this.informMutation();
|
||||
}
|
||||
|
@ -332,6 +365,22 @@ class Scene {
|
|||
getElementIndex(elementId: string) {
|
||||
return this.elements.findIndex((element) => element.id === elementId);
|
||||
}
|
||||
|
||||
getContainerElement = (
|
||||
element:
|
||||
| (ExcalidrawElement & {
|
||||
containerId: ExcalidrawElement["id"] | null;
|
||||
})
|
||||
| null,
|
||||
) => {
|
||||
if (!element) {
|
||||
return null;
|
||||
}
|
||||
if (element.containerId) {
|
||||
return this.getElement(element.containerId) || null;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export default Scene;
|
||||
|
|
|
@ -42,7 +42,8 @@ export const canChangeRoundness = (type: ElementOrToolType) =>
|
|||
type === "embeddable" ||
|
||||
type === "arrow" ||
|
||||
type === "line" ||
|
||||
type === "diamond";
|
||||
type === "diamond" ||
|
||||
type === "image";
|
||||
|
||||
export const canHaveArrowheads = (type: ElementOrToolType) => type === "arrow";
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import {
|
|||
ExcalidrawFrameLikeElement,
|
||||
ExcalidrawTextElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import {
|
||||
Bounds,
|
||||
|
@ -11,7 +12,13 @@ import {
|
|||
getElementAbsoluteCoords,
|
||||
} from "../element/bounds";
|
||||
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
||||
import { cloneJSON, distance, getFontString } from "../utils";
|
||||
import {
|
||||
arrayToMap,
|
||||
cloneJSON,
|
||||
distance,
|
||||
getFontString,
|
||||
toBrandedType,
|
||||
} from "../utils";
|
||||
import { AppState, BinaryFiles } from "../types";
|
||||
import {
|
||||
DEFAULT_EXPORT_PADDING,
|
||||
|
@ -26,8 +33,8 @@ import {
|
|||
getInitializedImageElements,
|
||||
updateImageCache,
|
||||
} from "../element/image";
|
||||
import { elementsOverlappingBBox } from "../../utils/export";
|
||||
import {
|
||||
getElementsOverlappingFrame,
|
||||
getFrameLikeElements,
|
||||
getFrameLikeTitle,
|
||||
getRootElements,
|
||||
|
@ -37,6 +44,7 @@ import { Mutable } from "../utility-types";
|
|||
import { newElementWith } from "../element/mutateElement";
|
||||
import Scene from "./Scene";
|
||||
import { isFrameElement, isFrameLikeElement } from "../element/typeChecks";
|
||||
import { RenderableElementsMap } from "./types";
|
||||
|
||||
const SVG_EXPORT_TAG = `<!-- svg-source:excalidraw -->`;
|
||||
|
||||
|
@ -168,11 +176,7 @@ const prepareElementsForRender = ({
|
|||
let nextElements: readonly ExcalidrawElement[];
|
||||
|
||||
if (exportingFrame) {
|
||||
nextElements = elementsOverlappingBBox({
|
||||
elements,
|
||||
bounds: exportingFrame,
|
||||
type: "overlap",
|
||||
});
|
||||
nextElements = getElementsOverlappingFrame(elements, exportingFrame);
|
||||
} else if (frameRendering.enabled && frameRendering.name) {
|
||||
nextElements = addFrameLabelsAsTextElements(elements, {
|
||||
exportWithDarkMode,
|
||||
|
@ -248,7 +252,12 @@ export const exportToCanvas = async (
|
|||
renderStaticScene({
|
||||
canvas,
|
||||
rc: rough.canvas(canvas),
|
||||
elements: elementsForRender,
|
||||
elementsMap: toBrandedType<RenderableElementsMap>(
|
||||
arrayToMap(elementsForRender),
|
||||
),
|
||||
allElementsMap: toBrandedType<NonDeletedSceneElementsMap>(
|
||||
arrayToMap(elements),
|
||||
),
|
||||
visibleElements: elementsForRender,
|
||||
scale,
|
||||
appState: {
|
||||
|
@ -436,22 +445,29 @@ export const exportToSvg = async (
|
|||
|
||||
const renderEmbeddables = opts?.renderEmbeddables ?? false;
|
||||
|
||||
renderSceneToSvg(elementsForRender, rsvg, svgRoot, files || {}, {
|
||||
offsetX,
|
||||
offsetY,
|
||||
isExporting: true,
|
||||
exportWithDarkMode,
|
||||
renderEmbeddables,
|
||||
frameRendering,
|
||||
canvasBackgroundColor: viewBackgroundColor,
|
||||
embedsValidationStatus: renderEmbeddables
|
||||
? new Map(
|
||||
elementsForRender
|
||||
.filter((element) => isFrameLikeElement(element))
|
||||
.map((element) => [element.id, true]),
|
||||
)
|
||||
: new Map(),
|
||||
});
|
||||
renderSceneToSvg(
|
||||
elementsForRender,
|
||||
toBrandedType<RenderableElementsMap>(arrayToMap(elementsForRender)),
|
||||
rsvg,
|
||||
svgRoot,
|
||||
files || {},
|
||||
{
|
||||
offsetX,
|
||||
offsetY,
|
||||
isExporting: true,
|
||||
exportWithDarkMode,
|
||||
renderEmbeddables,
|
||||
frameRendering,
|
||||
canvasBackgroundColor: viewBackgroundColor,
|
||||
embedsValidationStatus: renderEmbeddables
|
||||
? new Map(
|
||||
elementsForRender
|
||||
.filter((element) => isFrameLikeElement(element))
|
||||
.map((element) => [element.id, true]),
|
||||
)
|
||||
: new Map(),
|
||||
},
|
||||
);
|
||||
|
||||
tempScene.destroy();
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
export { isOverScrollBars } from "./scrollbars";
|
||||
export {
|
||||
isSomeElementSelected,
|
||||
getElementsWithinSelection,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { ExcalidrawElement } from "../element/types";
|
||||
import { getCommonBounds } from "../element";
|
||||
import { InteractiveCanvasAppState } from "../types";
|
||||
import { ScrollBars } from "./types";
|
||||
import { RenderableElementsMap, ScrollBars } from "./types";
|
||||
import { getGlobalCSSVariable } from "../utils";
|
||||
import { getLanguage } from "../i18n";
|
||||
|
||||
|
@ -10,12 +9,12 @@ export const SCROLLBAR_WIDTH = 6;
|
|||
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
|
||||
|
||||
export const getScrollBars = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
elements: RenderableElementsMap,
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
appState: InteractiveCanvasAppState,
|
||||
): ScrollBars => {
|
||||
if (elements.length === 0) {
|
||||
if (!elements.size) {
|
||||
return {
|
||||
horizontal: null,
|
||||
vertical: null,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
ElementsMapOrArray,
|
||||
ExcalidrawElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
|
@ -166,26 +167,28 @@ export const getCommonAttributeOfSelectedElements = <T>(
|
|||
};
|
||||
|
||||
export const getSelectedElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elements: ElementsMapOrArray,
|
||||
appState: Pick<InteractiveCanvasAppState, "selectedElementIds">,
|
||||
opts?: {
|
||||
includeBoundTextElement?: boolean;
|
||||
includeElementsInFrames?: boolean;
|
||||
},
|
||||
) => {
|
||||
const selectedElements = elements.filter((element) => {
|
||||
const selectedElements: ExcalidrawElement[] = [];
|
||||
for (const element of elements.values()) {
|
||||
if (appState.selectedElementIds[element.id]) {
|
||||
return element;
|
||||
selectedElements.push(element);
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
opts?.includeBoundTextElement &&
|
||||
isBoundToContainer(element) &&
|
||||
appState.selectedElementIds[element?.containerId]
|
||||
) {
|
||||
return element;
|
||||
selectedElements.push(element);
|
||||
continue;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
if (opts?.includeElementsInFrames) {
|
||||
const elementsToInclude: ExcalidrawElement[] = [];
|
||||
|
@ -205,7 +208,7 @@ export const getSelectedElements = (
|
|||
};
|
||||
|
||||
export const getTargetElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
elements: ElementsMapOrArray,
|
||||
appState: Pick<AppState, "selectedElementIds" | "editingElement">,
|
||||
) =>
|
||||
appState.editingElement
|
||||
|
|
|
@ -2,7 +2,9 @@ import type { RoughCanvas } from "roughjs/bin/canvas";
|
|||
import { Drawable } from "roughjs/bin/core";
|
||||
import {
|
||||
ExcalidrawTextElement,
|
||||
NonDeletedElementsMap,
|
||||
NonDeletedExcalidrawElement,
|
||||
NonDeletedSceneElementsMap,
|
||||
} from "../element/types";
|
||||
import {
|
||||
AppClassProperties,
|
||||
|
@ -12,6 +14,10 @@ import {
|
|||
InteractiveCanvasAppState,
|
||||
StaticCanvasAppState,
|
||||
} from "../types";
|
||||
import { MakeBrand } from "../utility-types";
|
||||
|
||||
export type RenderableElementsMap = NonDeletedElementsMap &
|
||||
MakeBrand<"RenderableElementsMap">;
|
||||
|
||||
export type StaticCanvasRenderConfig = {
|
||||
canvasBackgroundColor: AppState["viewBackgroundColor"];
|
||||
|
@ -53,14 +59,15 @@ export type InteractiveCanvasRenderConfig = {
|
|||
|
||||
export type RenderInteractiveSceneCallback = {
|
||||
atLeastOneVisibleElement: boolean;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
elementsMap: RenderableElementsMap;
|
||||
scrollBars?: ScrollBars;
|
||||
};
|
||||
|
||||
export type StaticSceneRenderConfig = {
|
||||
canvas: HTMLCanvasElement;
|
||||
rc: RoughCanvas;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
elementsMap: RenderableElementsMap;
|
||||
allElementsMap: NonDeletedSceneElementsMap;
|
||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||
scale: number;
|
||||
appState: StaticCanvasAppState;
|
||||
|
@ -69,7 +76,7 @@ export type StaticSceneRenderConfig = {
|
|||
|
||||
export type InteractiveSceneRenderConfig = {
|
||||
canvas: HTMLCanvasElement | null;
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
elementsMap: RenderableElementsMap;
|
||||
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||
scale: number;
|
||||
|
|
|
@ -16,6 +16,7 @@ import { KEYS } from "./keys";
|
|||
import { rangeIntersection, rangesOverlap, rotatePoint } from "./math";
|
||||
import { getVisibleAndNonSelectedElements } from "./scene/selection";
|
||||
import { AppState, KeyboardModifiersObject, Point } from "./types";
|
||||
import { arrayToMap } from "./utils";
|
||||
|
||||
const SNAP_DISTANCE = 8;
|
||||
|
||||
|
@ -286,7 +287,10 @@ export const getVisibleGaps = (
|
|||
appState,
|
||||
);
|
||||
|
||||
const referenceBounds = getMaximumGroups(referenceElements)
|
||||
const referenceBounds = getMaximumGroups(
|
||||
referenceElements,
|
||||
arrayToMap(elements),
|
||||
)
|
||||
.filter(
|
||||
(elementsGroup) =>
|
||||
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
|
||||
|
@ -572,7 +576,7 @@ export const getReferenceSnapPoints = (
|
|||
appState,
|
||||
);
|
||||
|
||||
return getMaximumGroups(referenceElements)
|
||||
return getMaximumGroups(referenceElements, arrayToMap(elements))
|
||||
.filter(
|
||||
(elementsGroup) =>
|
||||
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
|
||||
|
|
|
@ -21,5 +21,5 @@ exports[`export > exporting svg containing transformed images > svg export outpu
|
|||
</style>
|
||||
|
||||
</defs>
|
||||
<g transform="translate(30.710678118654755 30.710678118654755) rotate(315 50 50)" data-id="id1"><use href="#image-file_A" width="100" height="100" opacity="1"></use></g><g transform="translate(130.71067811865476 30.710678118654755) rotate(45 25 25)" data-id="id2"><use href="#image-file_A" width="50" height="50" opacity="1" transform="scale(-1, 1) translate(-50 0)"></use></g><g transform="translate(30.710678118654755 130.71067811865476) rotate(45 50 50)" data-id="id3"><use href="#image-file_A" width="100" height="100" opacity="1" transform="scale(1, -1) translate(0 -100)"></use></g><g transform="translate(130.71067811865476 130.71067811865476) rotate(315 25 25)" data-id="id4"><use href="#image-file_A" width="50" height="50" opacity="1" transform="scale(-1, -1) translate(-50 -50)"></use></g></svg>"
|
||||
<clipPath id="image-clipPath-id1" data-id="id1"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(30.710678118654755 30.710678118654755) rotate(315 50 50)" clip-path="url(#image-clipPath-id1)" data-id="id1"><use href="#image-file_A" width="100" height="100" opacity="1"></use></g><clipPath id="image-clipPath-id2" data-id="id2"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(130.71067811865476 30.710678118654755) rotate(45 25 25)" clip-path="url(#image-clipPath-id2)" data-id="id2"><use href="#image-file_A" width="50" height="50" opacity="1" transform="scale(-1, 1) translate(-50 0)"></use></g><clipPath id="image-clipPath-id3" data-id="id3"><rect width="100" height="100" rx="25" ry="25"></rect></clipPath><g transform="translate(30.710678118654755 130.71067811865476) rotate(45 50 50)" clip-path="url(#image-clipPath-id3)" data-id="id3"><use href="#image-file_A" width="100" height="100" opacity="1" transform="scale(1, -1) translate(0 -100)"></use></g><clipPath id="image-clipPath-id4" data-id="id4"><rect width="50" height="50" rx="12.5" ry="12.5"></rect></clipPath><g transform="translate(130.71067811865476 130.71067811865476) rotate(315 25 25)" clip-path="url(#image-clipPath-id4)" data-id="id4"><use href="#image-file_A" width="50" height="50" opacity="1" transform="scale(-1, -1) translate(-50 -50)"></use></g></svg>"
|
||||
`;
|
||||
|
|
|
@ -263,3 +263,170 @@ describe("Paste bound text container", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pasting & frames", () => {
|
||||
it("should add pasted elements to frame under cursor", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const rect = API.createElement({ type: "rectangle" });
|
||||
|
||||
h.elements = [frame];
|
||||
|
||||
const clipboardJSON = await serializeAsClipboardJSON({
|
||||
elements: [rect],
|
||||
files: null,
|
||||
});
|
||||
|
||||
mouse.moveTo(50, 50);
|
||||
|
||||
pasteWithCtrlCmdV(clipboardJSON);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements.length).toBe(2);
|
||||
expect(h.elements[1].type).toBe(rect.type);
|
||||
expect(h.elements[1].frameId).toBe(frame.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should filter out elements not overlapping frame", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 50,
|
||||
height: 50,
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 50,
|
||||
height: 50,
|
||||
x: 100,
|
||||
y: 100,
|
||||
});
|
||||
|
||||
h.elements = [frame];
|
||||
|
||||
const clipboardJSON = await serializeAsClipboardJSON({
|
||||
elements: [rect, rect2],
|
||||
files: null,
|
||||
});
|
||||
|
||||
mouse.moveTo(90, 90);
|
||||
|
||||
pasteWithCtrlCmdV(clipboardJSON);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements.length).toBe(3);
|
||||
expect(h.elements[1].type).toBe(rect.type);
|
||||
expect(h.elements[1].frameId).toBe(frame.id);
|
||||
expect(h.elements[2].type).toBe(rect2.type);
|
||||
expect(h.elements[2].frameId).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not filter out elements not overlapping frame if part of group", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 50,
|
||||
height: 50,
|
||||
groupIds: ["g1"],
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 50,
|
||||
height: 50,
|
||||
x: 100,
|
||||
y: 100,
|
||||
groupIds: ["g1"],
|
||||
});
|
||||
|
||||
h.elements = [frame];
|
||||
|
||||
const clipboardJSON = await serializeAsClipboardJSON({
|
||||
elements: [rect, rect2],
|
||||
files: null,
|
||||
});
|
||||
|
||||
mouse.moveTo(90, 90);
|
||||
|
||||
pasteWithCtrlCmdV(clipboardJSON);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements.length).toBe(3);
|
||||
expect(h.elements[1].type).toBe(rect.type);
|
||||
expect(h.elements[1].frameId).toBe(frame.id);
|
||||
expect(h.elements[2].type).toBe(rect2.type);
|
||||
expect(h.elements[2].frameId).toBe(frame.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should not filter out other frames and their children", async () => {
|
||||
const frame = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const rect = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 50,
|
||||
height: 50,
|
||||
groupIds: ["g1"],
|
||||
});
|
||||
|
||||
const frame2 = API.createElement({
|
||||
type: "frame",
|
||||
width: 75,
|
||||
height: 75,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const rect2 = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 50,
|
||||
height: 50,
|
||||
x: 55,
|
||||
y: 55,
|
||||
frameId: frame2.id,
|
||||
});
|
||||
|
||||
h.elements = [frame];
|
||||
|
||||
const clipboardJSON = await serializeAsClipboardJSON({
|
||||
elements: [rect, rect2, frame2],
|
||||
files: null,
|
||||
});
|
||||
|
||||
mouse.moveTo(90, 90);
|
||||
|
||||
pasteWithCtrlCmdV(clipboardJSON);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(h.elements.length).toBe(4);
|
||||
expect(h.elements[1].type).toBe(rect.type);
|
||||
expect(h.elements[1].frameId).toBe(frame.id);
|
||||
expect(h.elements[2].type).toBe(rect2.type);
|
||||
expect(h.elements[2].frameId).toBe(h.elements[3].id);
|
||||
expect(h.elements[3].type).toBe(frame2.type);
|
||||
expect(h.elements[3].frameId).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -206,6 +206,8 @@ export class Pointer {
|
|||
moveTo(x: number = this.clientX, y: number = this.clientY) {
|
||||
this.clientX = x;
|
||||
this.clientY = y;
|
||||
// fire "mousemove" to update editor cursor position
|
||||
fireEvent.mouseMove(document, this.getEvent());
|
||||
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
import * as textElementUtils from "../element/textElement";
|
||||
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||
import { vi } from "vitest";
|
||||
import { arrayToMap } from "../utils";
|
||||
|
||||
const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
|
||||
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||
|
@ -307,6 +308,7 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
const midPointsWithSharpEdge = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
|
||||
|
@ -320,6 +322,7 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
||||
h.elements[0] as ExcalidrawLinearElement,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
expect(midPointsWithRoundEdge[0]).not.toEqual(midPointsWithSharpEdge[0]);
|
||||
|
@ -351,7 +354,11 @@ describe("Test Linear Elements", () => {
|
|||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
expect([line.x, line.y]).toEqual(points[0]);
|
||||
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
|
||||
const startPoint = centerPoint(points[0], midPoints[0] as Point);
|
||||
const deltaX = 50;
|
||||
|
@ -373,6 +380,7 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
expect(midPoints[0]).not.toEqual(newMidPoints[0]);
|
||||
|
@ -458,7 +466,11 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
it("should update only the first segment midpoint when its point is dragged", async () => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
|
||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
||||
|
||||
|
@ -478,6 +490,7 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
|
||||
|
@ -487,7 +500,11 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
it("should hide midpoints in the segment when points moved close", async () => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
|
||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
||||
|
||||
|
@ -507,6 +524,7 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
// This midpoint is hidden since the points are too close
|
||||
|
@ -526,7 +544,11 @@ describe("Test Linear Elements", () => {
|
|||
]);
|
||||
expect(line.points.length).toEqual(4);
|
||||
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
|
||||
// delete 3rd point
|
||||
deletePoint(points[2]);
|
||||
|
@ -538,6 +560,7 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
expect(newMidPoints.length).toEqual(2);
|
||||
|
@ -615,7 +638,11 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
it("should update all the midpoints when its point is dragged", async () => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
|
||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
||||
|
||||
|
@ -630,6 +657,7 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
|
||||
|
@ -651,7 +679,11 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
it("should hide midpoints in the segment when points moved close", async () => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
|
||||
const hitCoords: Point = [points[0][0], points[0][1]];
|
||||
|
||||
|
@ -671,6 +703,7 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
// This mid point is hidden due to point being too close
|
||||
|
@ -685,7 +718,11 @@ describe("Test Linear Elements", () => {
|
|||
]);
|
||||
expect(line.points.length).toEqual(4);
|
||||
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(line, h.state);
|
||||
const midPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||
|
||||
// delete 3rd point
|
||||
|
@ -694,6 +731,7 @@ describe("Test Linear Elements", () => {
|
|||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
h.state,
|
||||
);
|
||||
expect(newMidPoints.length).toEqual(2);
|
||||
|
@ -762,7 +800,7 @@ describe("Test Linear Elements", () => {
|
|||
type: "text",
|
||||
x: 0,
|
||||
y: 0,
|
||||
text: wrapText(text, font, getBoundTextMaxWidth(container)),
|
||||
text: wrapText(text, font, getBoundTextMaxWidth(container, null)),
|
||||
containerId: container.id,
|
||||
width: 30,
|
||||
height: 20,
|
||||
|
@ -956,70 +994,6 @@ describe("Test Linear Elements", () => {
|
|||
expect(line.boundElements).toBeNull();
|
||||
});
|
||||
|
||||
it("should not rotate the bound text and update position of bound text and bounding box correctly when arrow rotated", () => {
|
||||
createThreePointerLinearElement("arrow", {
|
||||
type: ROUNDNESS.PROPORTIONAL_RADIUS,
|
||||
});
|
||||
|
||||
const arrow = h.elements[0] as ExcalidrawLinearElement;
|
||||
|
||||
const { textElement, container } = createBoundTextElement(
|
||||
DEFAULT_TEXT,
|
||||
arrow,
|
||||
);
|
||||
|
||||
expect(container.angle).toBe(0);
|
||||
expect(textElement.angle).toBe(0);
|
||||
expect(getBoundTextElementPosition(arrow, textElement))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"x": 75,
|
||||
"y": 60,
|
||||
}
|
||||
`);
|
||||
expect(textElement.text).toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
|
||||
.toMatchInlineSnapshot(`
|
||||
[
|
||||
20,
|
||||
20,
|
||||
105,
|
||||
80,
|
||||
55.45893770831013,
|
||||
45,
|
||||
]
|
||||
`);
|
||||
|
||||
expect(container.angle).toMatchInlineSnapshot("0");
|
||||
expect(textElement.angle).toBe(0);
|
||||
expect(getBoundTextElementPosition(container, textElement))
|
||||
.toMatchInlineSnapshot(`
|
||||
{
|
||||
"x": 75,
|
||||
"y": 60,
|
||||
}
|
||||
`);
|
||||
expect(textElement.text).toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
|
||||
.toMatchInlineSnapshot(`
|
||||
[
|
||||
20,
|
||||
20,
|
||||
105,
|
||||
80,
|
||||
55.45893770831013,
|
||||
45,
|
||||
]
|
||||
`);
|
||||
});
|
||||
// TODO fix #7029 and rewrite this test
|
||||
it.todo(
|
||||
"should not rotate the bound text and update position of bound text and bounding box correctly when arrow rotated",
|
||||
|
@ -1050,8 +1024,13 @@ describe("Test Linear Elements", () => {
|
|||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(
|
||||
LinearElementEditor.getElementAbsoluteCoords(
|
||||
container,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
true,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
20,
|
||||
20,
|
||||
|
@ -1084,8 +1063,13 @@ describe("Test Linear Elements", () => {
|
|||
"Online whiteboard
|
||||
collaboration made easy"
|
||||
`);
|
||||
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
|
||||
.toMatchInlineSnapshot(`
|
||||
expect(
|
||||
LinearElementEditor.getElementAbsoluteCoords(
|
||||
container,
|
||||
h.app.scene.getNonDeletedElementsMap(),
|
||||
true,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
20,
|
||||
35,
|
||||
|
@ -1185,7 +1169,11 @@ describe("Test Linear Elements", () => {
|
|||
expect(rect.x).toBe(400);
|
||||
expect(rect.y).toBe(0);
|
||||
expect(
|
||||
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
||||
wrapText(
|
||||
textElement.originalText,
|
||||
font,
|
||||
getBoundTextMaxWidth(arrow, null),
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made easy"
|
||||
|
@ -1204,11 +1192,17 @@ describe("Test Linear Elements", () => {
|
|||
expect(rect.x).toBe(200);
|
||||
expect(rect.y).toBe(0);
|
||||
expect(handleBindTextResizeSpy).toHaveBeenCalledWith(
|
||||
h.elements[1],
|
||||
h.elements[0],
|
||||
arrayToMap(h.elements),
|
||||
"nw",
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)),
|
||||
wrapText(
|
||||
textElement.originalText,
|
||||
font,
|
||||
getBoundTextMaxWidth(arrow, null),
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
|
|
|
@ -406,5 +406,67 @@ describe("exporting frames", () => {
|
|||
(frame.height + getFrameNameHeight("svg")).toString(),
|
||||
);
|
||||
});
|
||||
|
||||
it("should not export frame-overlapping elements belonging to different frame", async () => {
|
||||
const frame1 = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
const frame2 = API.createElement({
|
||||
type: "frame",
|
||||
width: 100,
|
||||
height: 100,
|
||||
x: 200,
|
||||
y: 0,
|
||||
});
|
||||
|
||||
const frame1Child = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 150,
|
||||
height: 100,
|
||||
x: 0,
|
||||
y: 50,
|
||||
frameId: frame1.id,
|
||||
});
|
||||
const frame2Child = API.createElement({
|
||||
type: "rectangle",
|
||||
width: 150,
|
||||
height: 100,
|
||||
x: 50,
|
||||
y: 0,
|
||||
frameId: frame2.id,
|
||||
});
|
||||
|
||||
// low-level exportToSvg api expects elements to be pre-filtered, so let's
|
||||
// use the filter we use in the editor
|
||||
const { exportedElements, exportingFrame } = prepareElementsForExport(
|
||||
[frame1Child, frame1, frame2Child, frame2],
|
||||
{
|
||||
selectedElementIds: { [frame1.id]: true },
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
const svg = await exportToSvg({
|
||||
elements: exportedElements,
|
||||
files: null,
|
||||
exportPadding: 0,
|
||||
exportingFrame,
|
||||
});
|
||||
|
||||
// frame shouldn't be exported
|
||||
expect(svg.querySelector(`[data-id="${frame1.id}"]`)).toBeNull();
|
||||
// frame1 child should be epxorted
|
||||
expect(svg.querySelector(`[data-id="${frame1Child.id}"]`)).not.toBeNull();
|
||||
// frame2 child should not be exported even if it physically overlaps with
|
||||
// frame1
|
||||
expect(svg.querySelector(`[data-id="${frame2Child.id}"]`)).toBeNull();
|
||||
|
||||
expect(svg.getAttribute("width")).toBe(frame1.width.toString());
|
||||
expect(svg.getAttribute("height")).toBe(frame1.height.toString());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"exclude": ["**/*.test.*", "tests", "types", "example", "dist"],
|
||||
"exclude": ["**/*.test.*", "tests", "types", "examples", "dist"],
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"strict": true,
|
||||
|
|
|
@ -31,7 +31,7 @@ import type { throttleRAF } from "./utils";
|
|||
import { Spreadsheet } from "./charts";
|
||||
import { Language } from "./i18n";
|
||||
import { ClipboardData } from "./clipboard";
|
||||
import { isOverScrollBars } from "./scene";
|
||||
import { isOverScrollBars } from "./scene/scrollbars";
|
||||
import { MaybeTransformHandleType } from "./element/transformHandles";
|
||||
import Library from "./data/library";
|
||||
import type { FileSystemHandle } from "./data/filesystem";
|
||||
|
@ -457,6 +457,10 @@ export interface ExcalidrawProps {
|
|||
activeTool: AppState["activeTool"],
|
||||
pointerDownState: PointerDownState,
|
||||
) => void;
|
||||
onPointerUp?: (
|
||||
activeTool: AppState["activeTool"],
|
||||
pointerDownState: PointerDownState,
|
||||
) => void;
|
||||
onScrollChange?: (scrollX: number, scrollY: number, zoom: Zoom) => void;
|
||||
onUserFollow?: (payload: OnUserFollowedPayload) => void;
|
||||
children?: React.ReactNode;
|
||||
|
@ -493,7 +497,6 @@ export type ExportOpts = {
|
|||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: UIAppState,
|
||||
files: BinaryFiles,
|
||||
canvas: HTMLCanvasElement,
|
||||
) => void;
|
||||
renderCustomUI?: (
|
||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||
|
|
|
@ -54,3 +54,11 @@ export type Assert<T extends true> = T;
|
|||
export type NestedKeyOf<T, K = keyof T> = K extends keyof T & (string | number)
|
||||
? `${K}` | (T[K] extends object ? `${K}.${NestedKeyOf<T[K]>}` : never)
|
||||
: never;
|
||||
|
||||
export type SetLike<T> = Set<T> | T[];
|
||||
export type ReadonlySetLike<T> = ReadonlySet<T> | readonly T[];
|
||||
|
||||
export type MakeBrand<T extends string> = {
|
||||
/** @private using ~ to sort last in intellisense */
|
||||
[K in `~brand~${T}`]: T;
|
||||
};
|
||||
|
|
|
@ -650,8 +650,11 @@ export const getUpdatedTimestamp = () => (isTestEnv() ? 1 : Date.now());
|
|||
* or array of ids (strings), into a Map, keyd by `id`.
|
||||
*/
|
||||
export const arrayToMap = <T extends { id: string } | string>(
|
||||
items: readonly T[],
|
||||
items: readonly T[] | Map<string, T>,
|
||||
) => {
|
||||
if (items instanceof Map) {
|
||||
return items;
|
||||
}
|
||||
return items.reduce((acc: Map<string, T>, element) => {
|
||||
acc.set(typeof element === "string" ? element : element.id, element);
|
||||
return acc;
|
||||
|
@ -842,7 +845,7 @@ export const composeEventHandlers = <E>(
|
|||
|
||||
if (
|
||||
!checkForDefaultPrevented ||
|
||||
!(event as unknown as Event).defaultPrevented
|
||||
!(event as unknown as Event)?.defaultPrevented
|
||||
) {
|
||||
return ourEventHandler?.(event);
|
||||
}
|
||||
|
@ -1050,3 +1053,40 @@ export function getSvgPathFromStroke(points: number[][], closed = true) {
|
|||
export const normalizeEOL = (str: string) => {
|
||||
return str.replace(/\r?\n|\r/g, "\n");
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
type HasBrand<T> = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
[K in keyof T]: K extends `~brand${infer _}` ? true : never;
|
||||
}[keyof T];
|
||||
|
||||
type RemoveAllBrands<T> = HasBrand<T> extends true
|
||||
? {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
[K in keyof T as K extends `~brand~${infer _}` ? never : K]: T[K];
|
||||
}
|
||||
: never;
|
||||
|
||||
// adapted from https://github.com/colinhacks/zod/discussions/1994#discussioncomment-6068940
|
||||
// currently does not cover all types (e.g. tuples, promises...)
|
||||
type Unbrand<T> = T extends Map<infer E, infer F>
|
||||
? Map<E, F>
|
||||
: T extends Set<infer E>
|
||||
? Set<E>
|
||||
: T extends Array<infer E>
|
||||
? Array<E>
|
||||
: RemoveAllBrands<T>;
|
||||
|
||||
/**
|
||||
* Makes type into a branded type, ensuring that value is assignable to
|
||||
* the base ubranded type. Optionally you can explicitly supply current value
|
||||
* type to combine both (useful for composite branded types. Make sure you
|
||||
* compose branded types which are not composite themselves.)
|
||||
*/
|
||||
export const toBrandedType = <BrandedType, CurrentType = BrandedType>(
|
||||
value: Unbrand<BrandedType>,
|
||||
) => {
|
||||
return value as CurrentType & BrandedType;
|
||||
};
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue