Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2ed78d4895 | |||
| 5ba2798306 | |||
| b8aae34e32 | |||
| 6317ac16ee | |||
| 9ad75b8375 | |||
| 4c9ad1a22f | |||
| 023895b49b | |||
| eb37be953a | |||
| b4903a7eab | |||
| c6f8ef9ad2 | |||
| 2535d73054 | |||
| dda3affcb0 | |||
| 54c148f390 | |||
| cc8e490c75 | |||
| 9036812b6d | |||
| df25de7e68 | |||
| a3763648fe |
@@ -0,0 +1,27 @@
|
|||||||
|
name: Playwright Tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main, master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
timeout-minutes: 60
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install -g yarn && yarn
|
||||||
|
- name: Install Playwright Browsers
|
||||||
|
run: yarn playwright install --with-deps
|
||||||
|
- name: Run Playwright tests
|
||||||
|
run: yarn playwright test
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
with:
|
||||||
|
name: playwright-report
|
||||||
|
path: playwright-report/
|
||||||
|
retention-days: 30
|
||||||
@@ -27,3 +27,9 @@ dev-dist
|
|||||||
html
|
html
|
||||||
meta*.json
|
meta*.json
|
||||||
.claude
|
.claude
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/test-results/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ import type {
|
|||||||
import type { ResolutionType } from "@excalidraw/common/utility-types";
|
import type { ResolutionType } from "@excalidraw/common/utility-types";
|
||||||
import type { ResolvablePromise } from "@excalidraw/common/utils";
|
import type { ResolvablePromise } from "@excalidraw/common/utils";
|
||||||
|
|
||||||
|
import "./record";
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
import {
|
import {
|
||||||
Provider,
|
Provider,
|
||||||
|
|||||||
@@ -259,7 +259,9 @@ export const loadFromFirebase = async (
|
|||||||
}
|
}
|
||||||
const storedScene = docSnap.data() as FirebaseStoredScene;
|
const storedScene = docSnap.data() as FirebaseStoredScene;
|
||||||
const elements = getSyncableElements(
|
const elements = getSyncableElements(
|
||||||
restoreElements(await decryptElements(storedScene, roomKey), null),
|
restoreElements(await decryptElements(storedScene, roomKey), null, {
|
||||||
|
deleteInvisibleElements: true,
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (socket) {
|
if (socket) {
|
||||||
|
|||||||
@@ -258,11 +258,16 @@ export const loadScene = async (
|
|||||||
await importFromBackend(id, privateKey),
|
await importFromBackend(id, privateKey),
|
||||||
localDataState?.appState,
|
localDataState?.appState,
|
||||||
localDataState?.elements,
|
localDataState?.elements,
|
||||||
{ repairBindings: true, refreshDimensions: false },
|
{
|
||||||
|
repairBindings: true,
|
||||||
|
refreshDimensions: false,
|
||||||
|
deleteInvisibleElements: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
data = restore(localDataState || null, null, null, {
|
data = restore(localDataState || null, null, null, {
|
||||||
repairBindings: true,
|
repairBindings: true,
|
||||||
|
deleteInvisibleElements: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Excalidraw | Hand-drawn look & feel • Collaborative • Secure</title>
|
<title>
|
||||||
|
Free, collaborative whiteboard • Hand-drawn look & feel | Excalidraw
|
||||||
|
</title>
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
|
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
|
||||||
@@ -14,7 +16,7 @@
|
|||||||
<!-- Primary Meta Tags -->
|
<!-- Primary Meta Tags -->
|
||||||
<meta
|
<meta
|
||||||
name="title"
|
name="title"
|
||||||
content="Excalidraw — Collaborative whiteboarding made easy"
|
content="Free, collaborative whiteboard • Hand-drawn look & feel | Excalidraw"
|
||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
name="description"
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"build:version": "node ../scripts/build-version.js",
|
"build:version": "node ../scripts/build-version.js",
|
||||||
"build": "yarn build:app && yarn build:version",
|
"build": "yarn build:app && yarn build:version",
|
||||||
"start": "yarn && vite",
|
"start": "yarn && vite",
|
||||||
|
"start:test": "yarn && vite --mode test",
|
||||||
"start:production": "yarn build && yarn serve",
|
"start:production": "yarn build && yarn serve",
|
||||||
"serve": "npx http-server build -a localhost -p 5001 -o",
|
"serve": "npx http-server build -a localhost -p 5001 -o",
|
||||||
"build:preview": "yarn build && vite preview --port 5000"
|
"build:preview": "yarn build && vite preview --port 5000"
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
import { isDevEnv } from "@excalidraw/common";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
record: typeof Record;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Record {
|
||||||
|
private static recording: boolean = false;
|
||||||
|
private static events: string = "";
|
||||||
|
private static timestamp: number = 0;
|
||||||
|
|
||||||
|
public static get isRecording() {
|
||||||
|
return Record.recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static header() {
|
||||||
|
Record.events += " await page.addInitScript(() => {\n";
|
||||||
|
Record.events += " Math.random = () => 0.42;\n\n";
|
||||||
|
|
||||||
|
// Capture LocalStorage, which is essential to re-establish state
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
const key = localStorage.key(i);
|
||||||
|
if (key != null) {
|
||||||
|
const value = JSON.stringify(localStorage.getItem(key));
|
||||||
|
Record.events += ` localStorage.getItem("${key}");\n`;
|
||||||
|
Record.events += ` localStorage.setItem("${key}", ${value});\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Record.events += " });\n";
|
||||||
|
Record.events += ` await page.setViewportSize({ width: ${window.innerWidth}, height: ${window.innerHeight} });\n`;
|
||||||
|
Record.events += ` await page.goto("http://localhost:3000");\n`;
|
||||||
|
Record.events += ` await page.waitForLoadState("load");\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static restart() {
|
||||||
|
if (!Record.recording) {
|
||||||
|
Record.start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Record.events += `});\n\n`;
|
||||||
|
Record.events += `test("${
|
||||||
|
Date.now() + Math.floor(Math.random() * Date.now()).toString(36)
|
||||||
|
}", async ({ page }) => {\n`;
|
||||||
|
|
||||||
|
Record.header();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static start() {
|
||||||
|
Record.recording = true;
|
||||||
|
|
||||||
|
// Record header
|
||||||
|
this.header();
|
||||||
|
|
||||||
|
// Set up the events
|
||||||
|
Record.timestamp = performance.now();
|
||||||
|
|
||||||
|
window.addEventListener("mousemove", this.onMouseMove);
|
||||||
|
window.addEventListener("mousedown", this.onMouseDown);
|
||||||
|
window.addEventListener("mouseup", this.onMouseUp);
|
||||||
|
window.addEventListener("keydown", this.onKeyDown);
|
||||||
|
window.addEventListener("keyup", this.onKeyUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static stop() {
|
||||||
|
window.removeEventListener("mousemove", this.onMouseMove);
|
||||||
|
window.removeEventListener("mousedown", this.onMouseDown);
|
||||||
|
window.removeEventListener("mouseup", this.onMouseUp);
|
||||||
|
window.removeEventListener("keydown", this.onKeyDown);
|
||||||
|
window.removeEventListener("keyup", this.onKeyUp);
|
||||||
|
Record.recording = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays a window as an absolutely positioned DIV with the generated
|
||||||
|
/// events within <pre> tags as formatted JSON, so it can be copied easily.
|
||||||
|
public static showGeneratedEvents() {
|
||||||
|
if (Record.recording) {
|
||||||
|
Record.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
const div = document.createElement("div");
|
||||||
|
div.style.position = "absolute";
|
||||||
|
div.style.top = "10px";
|
||||||
|
div.style.right = "10px";
|
||||||
|
div.style.left = "10px";
|
||||||
|
div.style.height = "60vh";
|
||||||
|
div.style.backgroundColor = "gray";
|
||||||
|
div.style.padding = "10px";
|
||||||
|
div.style.zIndex = "10000";
|
||||||
|
|
||||||
|
const pre = document.createElement("pre");
|
||||||
|
|
||||||
|
let textContent = `import { expect, test } from "@playwright/test";\n\n`;
|
||||||
|
textContent += `test("${
|
||||||
|
Date.now() + Math.floor(Math.random() * Date.now()).toString(36)
|
||||||
|
}", async ({ page }) => {\n`;
|
||||||
|
textContent += Record.events;
|
||||||
|
textContent += `});\n`;
|
||||||
|
|
||||||
|
pre.textContent = textContent;
|
||||||
|
//pre.textContent = Record.events;
|
||||||
|
|
||||||
|
pre.style.marginTop = "18px";
|
||||||
|
pre.style.maxHeight = "60vh";
|
||||||
|
pre.style.overflow = "auto";
|
||||||
|
div.appendChild(pre);
|
||||||
|
|
||||||
|
const copyBtn = document.createElement("button");
|
||||||
|
copyBtn.textContent = "Copy";
|
||||||
|
copyBtn.title = "Copy generated events to clipboard";
|
||||||
|
copyBtn.setAttribute("aria-label", "Copy generated events to clipboard");
|
||||||
|
copyBtn.style.position = "absolute";
|
||||||
|
copyBtn.style.top = "4px";
|
||||||
|
copyBtn.style.left = "4px";
|
||||||
|
copyBtn.style.border = "none";
|
||||||
|
copyBtn.style.background = "transparent";
|
||||||
|
copyBtn.style.fontSize = "12px";
|
||||||
|
copyBtn.style.lineHeight = "1";
|
||||||
|
copyBtn.style.cursor = "pointer";
|
||||||
|
copyBtn.style.padding = "4px 8px";
|
||||||
|
copyBtn.addEventListener("click", async () => {
|
||||||
|
const text = pre.textContent ?? "";
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
await navigator.clipboard.writeText(text);
|
||||||
|
} else {
|
||||||
|
const ta = document.createElement("textarea");
|
||||||
|
ta.value = text;
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
|
const orig = copyBtn.textContent;
|
||||||
|
copyBtn.textContent = "Copied";
|
||||||
|
setTimeout(() => (copyBtn.textContent = orig), 1000);
|
||||||
|
} catch {}
|
||||||
|
});
|
||||||
|
div.appendChild(copyBtn);
|
||||||
|
|
||||||
|
const closeBtn = document.createElement("button");
|
||||||
|
closeBtn.textContent = "×";
|
||||||
|
closeBtn.title = "Close";
|
||||||
|
closeBtn.style.position = "absolute";
|
||||||
|
closeBtn.style.top = "4px";
|
||||||
|
closeBtn.style.right = "4px";
|
||||||
|
closeBtn.style.border = "none";
|
||||||
|
closeBtn.style.background = "transparent";
|
||||||
|
closeBtn.style.fontSize = "18px";
|
||||||
|
closeBtn.style.lineHeight = "1";
|
||||||
|
closeBtn.style.cursor = "pointer";
|
||||||
|
closeBtn.addEventListener("click", () => {
|
||||||
|
// remove the dialog from DOM
|
||||||
|
if (div.parentNode) {
|
||||||
|
div.parentNode.removeChild(div);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
div.appendChild(closeBtn);
|
||||||
|
|
||||||
|
document.body.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static onMouseMove(event: MouseEvent) {
|
||||||
|
if (
|
||||||
|
event.clientX < 0 ||
|
||||||
|
event.clientX > window.innerWidth ||
|
||||||
|
event.clientY < 0 ||
|
||||||
|
event.clientY > window.innerHeight
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = event.timeStamp || performance.now();
|
||||||
|
const delay = now - Record.timestamp;
|
||||||
|
Record.timestamp = now;
|
||||||
|
|
||||||
|
if (delay > 0) {
|
||||||
|
Record.events += ` await page.waitForTimeout(${delay});\n`;
|
||||||
|
}
|
||||||
|
Record.events += ` await page.mouse.move(${event.clientX}, ${event.clientY});\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static onMouseDown(event: MouseEvent) {
|
||||||
|
const now = event.timeStamp || performance.now();
|
||||||
|
const delay = now - Record.timestamp;
|
||||||
|
Record.timestamp = now;
|
||||||
|
|
||||||
|
if (delay > 0) {
|
||||||
|
Record.events += ` await page.waitForTimeout(${delay});\n`;
|
||||||
|
}
|
||||||
|
const button =
|
||||||
|
event.button === 0 ? "left" : event.button === 1 ? "middle" : "right";
|
||||||
|
Record.events += ` await page.mouse.down({ button: "${button}" });\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static onMouseUp(event: MouseEvent) {
|
||||||
|
const now = event.timeStamp || performance.now();
|
||||||
|
const delay = now - Record.timestamp;
|
||||||
|
Record.timestamp = now;
|
||||||
|
|
||||||
|
if (delay > 0) {
|
||||||
|
Record.events += ` await page.waitForTimeout(${delay});\n`;
|
||||||
|
}
|
||||||
|
const button =
|
||||||
|
event.button === 0 ? "left" : event.button === 1 ? "middle" : "right";
|
||||||
|
Record.events += ` await page.mouse.up({ button: "${button}" });\n`;
|
||||||
|
|
||||||
|
Record.events += " await expect(page).toHaveScreenshot({\n";
|
||||||
|
Record.events += " maxDiffPixels: 100,\n";
|
||||||
|
Record.events += " maxDiffPixelRatio: 0.01,\n";
|
||||||
|
Record.events += " });\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static onKeyDown(event: KeyboardEvent) {
|
||||||
|
// Only record if the recording key is not pressed
|
||||||
|
if (event.key !== "F2") {
|
||||||
|
const now = event.timeStamp || performance.now();
|
||||||
|
const delay = now - Record.timestamp;
|
||||||
|
Record.timestamp = now;
|
||||||
|
|
||||||
|
if (delay > 0) {
|
||||||
|
Record.events += ` await page.waitForTimeout(${delay});\n`;
|
||||||
|
}
|
||||||
|
Record.events += ` await page.keyboard.down("${event.key}");\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static onKeyUp(event: KeyboardEvent) {
|
||||||
|
// Only record if the recording key is not pressed
|
||||||
|
if (event.key !== "F2") {
|
||||||
|
const now = event.timeStamp || performance.now();
|
||||||
|
const delay = now - Record.timestamp;
|
||||||
|
Record.timestamp = now;
|
||||||
|
|
||||||
|
if (delay > 0) {
|
||||||
|
Record.events += ` await page.waitForTimeout(${delay});\n`;
|
||||||
|
}
|
||||||
|
Record.events += ` await page.keyboard.up("${event.key}");\n`;
|
||||||
|
|
||||||
|
Record.events += " await expect(page).toHaveScreenshot({\n";
|
||||||
|
Record.events += " maxDiffPixels: 100,\n";
|
||||||
|
Record.events += " maxDiffPixelRatio: 0.01,\n";
|
||||||
|
Record.events += " });\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDevEnv()) {
|
||||||
|
window.record = Record;
|
||||||
|
|
||||||
|
window.addEventListener("keyup", (event) => {
|
||||||
|
if (event.key === "F2") {
|
||||||
|
if (Record.isRecording) {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
console.info("Stopping Playwright recording");
|
||||||
|
Record.stop();
|
||||||
|
} else {
|
||||||
|
Record.restart();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.info("Starting Playwright recording");
|
||||||
|
Record.start();
|
||||||
|
}
|
||||||
|
} else if (event.key === "Enter" && event.ctrlKey) {
|
||||||
|
Record.showGeneratedEvents();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,716 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("17562123239901g67cqde", async ({ page }) => {
|
||||||
|
await page.addInitScript(() => {
|
||||||
|
Math.random = () => 0.42;
|
||||||
|
|
||||||
|
localStorage.getItem("i18nextLng");
|
||||||
|
localStorage.setItem("i18nextLng", "en");
|
||||||
|
localStorage.getItem("excalidraw-collab");
|
||||||
|
localStorage.setItem("excalidraw-collab", '{"username":""}');
|
||||||
|
localStorage.getItem("excalidraw-debug");
|
||||||
|
localStorage.setItem("excalidraw-debug", '{"enabled":true}');
|
||||||
|
localStorage.getItem("excalidraw-theme");
|
||||||
|
localStorage.setItem("excalidraw-theme", "dark");
|
||||||
|
localStorage.getItem("version-files");
|
||||||
|
localStorage.setItem("version-files", "1756212319038");
|
||||||
|
localStorage.getItem("version-dataState");
|
||||||
|
localStorage.setItem("version-dataState", "1756212319038");
|
||||||
|
localStorage.getItem("excalidraw-state");
|
||||||
|
localStorage.setItem(
|
||||||
|
"excalidraw-state",
|
||||||
|
'{"showWelcomeScreen":true,"theme":"dark","currentChartType":"bar","currentItemBackgroundColor":"#a5d8ff","currentItemEndArrowhead":"arrow","currentItemFillStyle":"solid","currentItemFontFamily":5,"currentItemFontSize":20,"currentItemOpacity":100,"currentItemRoughness":2,"currentItemStartArrowhead":null,"currentItemStrokeColor":"#1e1e1e","currentItemRoundness":"round","currentItemArrowType":"round","currentItemStrokeStyle":"solid","currentItemStrokeWidth":2,"currentItemTextAlign":"left","cursorButton":"up","editingGroupId":null,"activeTool":{"type":"arrow","customType":null,"locked":false,"fromSelection":false,"lastActiveTool":null},"penMode":false,"penDetected":false,"exportBackground":true,"exportScale":1,"exportEmbedScene":false,"exportWithDarkMode":false,"gridSize":20,"gridStep":5,"gridModeEnabled":false,"defaultSidebarDockedPreference":false,"lastPointerDownWith":"mouse","name":"Untitled-2025-07-28-1603","openMenu":null,"openSidebar":null,"previousSelectedElementIds":{},"scrolledOutside":false,"scrollX":688.1079394457738,"scrollY":349.585883261872,"selectedElementIds":{},"selectedGroupIds":{},"shouldCacheIgnoreZoom":false,"stats":{"open":true,"panels":3},"viewBackgroundColor":"#ffffff","zenModeEnabled":false,"zoom":{"value":1.331666},"selectedLinearElement":null,"objectsSnapModeEnabled":false,"lockedMultiSelections":{}}',
|
||||||
|
);
|
||||||
|
localStorage.getItem("excalidraw");
|
||||||
|
localStorage.setItem(
|
||||||
|
"excalidraw",
|
||||||
|
'[{"id":"hJuTw4QcwTsFtadNNnkLj","type":"rectangle","x":-100,"y":-100,"width":200,"height":200,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#a5d8ff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"index":"a1","roundness":{"type":3},"seed":43277494,"version":839,"versionNonce":1923298088,"isDeleted":false,"boundElements":[{"id":"0xSZCPMN8RzKiJOpvGaKB","type":"arrow"}],"updated":1756212298830,"link":null,"locked":false},{"id":"qXw5KqKvAjHRr5uwPi9B-","type":"rectangle","x":-523.1841597523046,"y":-129.52451989693097,"width":200,"height":200,"angle":0,"strokeColor":"#1e1e1e","backgroundColor":"#a5d8ff","fillStyle":"solid","strokeWidth":2,"strokeStyle":"solid","roughness":1,"opacity":100,"groupIds":[],"frameId":null,"index":"a2","roundness":{"type":3},"seed":392633439,"version":963,"versionNonce":210394408,"isDeleted":false,"boundElements":[{"id":"0xSZCPMN8RzKiJOpvGaKB","type":"arrow"}],"updated":1756212298830,"link":null,"locked":false}]',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await page.setViewportSize({ width: 1280, height: 1001 });
|
||||||
|
await page.goto("http://localhost:3000");
|
||||||
|
await page.waitForLoadState("load");
|
||||||
|
await page.waitForTimeout(3.599999999627471);
|
||||||
|
await page.mouse.move(425, 390);
|
||||||
|
await page.waitForTimeout(9.900000000372529);
|
||||||
|
await page.mouse.move(424, 390);
|
||||||
|
await page.waitForTimeout(51.09999999962747);
|
||||||
|
await page.mouse.move(423, 390);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(423, 392);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(422, 392);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(420, 393);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(420, 395);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(418, 395);
|
||||||
|
await page.waitForTimeout(6);
|
||||||
|
await page.mouse.move(417, 396);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(416, 396);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(416, 397);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(414, 397);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(414, 399);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(413, 399);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(413, 401);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(413, 402);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(411, 402);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(411, 403);
|
||||||
|
await page.waitForTimeout(32);
|
||||||
|
await page.mouse.move(412, 403);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(412, 403);
|
||||||
|
await page.waitForTimeout(4);
|
||||||
|
await page.mouse.move(412, 402);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(413, 402);
|
||||||
|
await page.waitForTimeout(0.900000000372529);
|
||||||
|
await page.mouse.move(413, 400);
|
||||||
|
await page.waitForTimeout(1.099999999627471);
|
||||||
|
await page.mouse.move(415, 400);
|
||||||
|
await page.waitForTimeout(7);
|
||||||
|
await page.mouse.move(416, 400);
|
||||||
|
await page.waitForTimeout(71);
|
||||||
|
await page.mouse.move(417, 400);
|
||||||
|
await page.waitForTimeout(450);
|
||||||
|
await page.mouse.move(417, 399);
|
||||||
|
await page.waitForTimeout(6);
|
||||||
|
await page.mouse.move(418, 399);
|
||||||
|
await page.waitForTimeout(6.1000000005587935);
|
||||||
|
await page.mouse.move(419, 399);
|
||||||
|
await page.mouse.down({ button: "left" });
|
||||||
|
await page.waitForTimeout(11.899999999441206);
|
||||||
|
await page.mouse.move(420, 399);
|
||||||
|
await page.waitForTimeout(69);
|
||||||
|
await page.mouse.move(421, 399);
|
||||||
|
await page.waitForTimeout(13);
|
||||||
|
await page.mouse.move(422, 399);
|
||||||
|
await page.waitForTimeout(7.1000000005587935);
|
||||||
|
await page.mouse.move(423, 399);
|
||||||
|
await page.waitForTimeout(5.3999999994412065);
|
||||||
|
await page.mouse.move(425, 399);
|
||||||
|
await page.waitForTimeout(0.5);
|
||||||
|
await page.mouse.move(427, 401);
|
||||||
|
await page.waitForTimeout(4);
|
||||||
|
await page.mouse.move(428, 401);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(428, 403);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(430, 403);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(432, 403);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(432, 404);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(433, 404);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(434, 404);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(436, 406);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(437, 406);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(438, 407);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(440, 407);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(442, 407);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(443, 409);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(445, 409);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(447, 411);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(451, 411);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(455, 411);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(457, 413);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(459, 413);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(461, 415);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(463, 417);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(467, 419);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(469, 419);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(471, 420);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(473, 424);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(477, 424);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(477, 426);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(478, 426);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(482, 428);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(484, 430);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(486, 430);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(488, 430);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(488, 432);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(489, 432);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(491, 432);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(493, 434);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(495, 434);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(499, 436);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(501, 436);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(502, 436);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(504, 438);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(506, 440);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(508, 440);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(510, 442);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(512, 444);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(514, 444);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(514, 445);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(515, 445);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(517, 445);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(517, 447);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(519, 448);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(521, 448);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(522, 450);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(524, 450);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(525, 450);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(527, 450);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(529, 450);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(530, 450);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(532, 450);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(533, 451);
|
||||||
|
await page.waitForTimeout(6);
|
||||||
|
await page.mouse.move(534, 451);
|
||||||
|
await page.waitForTimeout(3.1000000005587935);
|
||||||
|
await page.mouse.move(535, 451);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(535, 453);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(537, 453);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(538, 453);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(540, 453);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(540, 454);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(541, 454);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(542, 454);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(543, 454);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(544, 454);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(544, 455);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(546, 455);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(547, 455);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(548, 455);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(549, 455);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(551, 455);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(552, 455);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(554, 455);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(555, 455);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(557, 455);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(559, 457);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(560, 457);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(562, 459);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(564, 459);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(566, 459);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(567, 459);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(569, 459);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(570, 459);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(574, 460);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(575, 460);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(575, 462);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(577, 464);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(578, 464);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(580, 464);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(582, 465);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(584, 465);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(585, 465);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(586, 465);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(588, 467);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(590, 467);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(591, 467);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(591, 468);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(594, 468);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(595, 468);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(597, 468);
|
||||||
|
await page.waitForTimeout(2.7000000001862645);
|
||||||
|
await page.mouse.move(601, 470);
|
||||||
|
await page.waitForTimeout(1.400000000372529);
|
||||||
|
await page.mouse.move(602, 470);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(603, 470);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(605, 470);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(607, 470);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(608, 470);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(610, 470);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(612, 470);
|
||||||
|
await page.waitForTimeout(2.8999999994412065);
|
||||||
|
await page.mouse.move(613, 470);
|
||||||
|
await page.waitForTimeout(1.2000000001862645);
|
||||||
|
await page.mouse.move(613, 471);
|
||||||
|
await page.waitForTimeout(1.7999999998137355);
|
||||||
|
await page.mouse.move(614, 471);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(616, 473);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(618, 473);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(621, 473);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(623, 473);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(625, 473);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(626, 473);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(628, 473);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(629, 473);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(631, 473);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(634, 475);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(636, 475);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(638, 475);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(639, 475);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(640, 475);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(642, 475);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(644, 475);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(647, 475);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(649, 475);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(650, 475);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(652, 475);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(653, 475);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(655, 475);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(656, 476);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(658, 476);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(659, 476);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(660, 476);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(662, 476);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(663, 476);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(664, 476);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(666, 474);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(668, 474);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(670, 474);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(671, 474);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(673, 476);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(675, 476);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(678, 476);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(680, 476);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(682, 476);
|
||||||
|
await page.waitForTimeout(4);
|
||||||
|
await page.mouse.move(683, 476);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(684, 476);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(686, 476);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(690, 476);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(694, 478);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(696, 478);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(698, 478);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(699, 478);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(703, 478);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(705, 478);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(707, 478);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(708, 478);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(711, 478);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(713, 478);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(714, 478);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(716, 478);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(717, 478);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(719, 478);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(722, 478);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(724, 478);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(725, 478);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(726, 478);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(727, 478);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(733, 480);
|
||||||
|
await page.waitForTimeout(2.8999999994412065);
|
||||||
|
await page.mouse.move(734, 480);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(735, 480);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(737, 480);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(739, 480);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(740, 480);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(742, 480);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(744, 480);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(744, 482);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(745, 482);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(747, 482);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(748, 482);
|
||||||
|
await page.waitForTimeout(4);
|
||||||
|
await page.mouse.move(749, 482);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(751, 482);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(753, 482);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(755, 482);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(758, 482);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(760, 482);
|
||||||
|
await page.waitForTimeout(6);
|
||||||
|
await page.mouse.move(761, 483);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(763, 484);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(765, 484);
|
||||||
|
await page.waitForTimeout(2.8999999994412065);
|
||||||
|
await page.mouse.move(766, 485);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(767, 485);
|
||||||
|
await page.waitForTimeout(11);
|
||||||
|
await page.mouse.move(768, 485);
|
||||||
|
await page.waitForTimeout(12.100000000558794);
|
||||||
|
await page.mouse.move(769, 485);
|
||||||
|
await page.waitForTimeout(51.89999999944121);
|
||||||
|
await page.mouse.move(770, 485);
|
||||||
|
await page.waitForTimeout(9);
|
||||||
|
await page.mouse.move(771, 485);
|
||||||
|
await page.waitForTimeout(3.1000000005587935);
|
||||||
|
await page.mouse.move(772, 485);
|
||||||
|
await page.waitForTimeout(5);
|
||||||
|
await page.mouse.move(773, 485);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(774, 485);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(776, 487);
|
||||||
|
await page.waitForTimeout(4);
|
||||||
|
await page.mouse.move(777, 487);
|
||||||
|
await page.waitForTimeout(4.1000000005587935);
|
||||||
|
await page.mouse.move(778, 487);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(779, 487);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(780, 488);
|
||||||
|
await page.waitForTimeout(2.8999999994412065);
|
||||||
|
await page.mouse.move(781, 488);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(782, 488);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(784, 488);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(785, 488);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(786, 488);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(788, 488);
|
||||||
|
await page.waitForTimeout(8.899999999441206);
|
||||||
|
await page.mouse.move(789, 488);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(790, 488);
|
||||||
|
await page.waitForTimeout(5);
|
||||||
|
await page.mouse.move(791, 488);
|
||||||
|
await page.waitForTimeout(4);
|
||||||
|
await page.mouse.move(792, 488);
|
||||||
|
await page.waitForTimeout(6.1000000005587935);
|
||||||
|
await page.mouse.move(793, 488);
|
||||||
|
await page.waitForTimeout(3.8999999994412065);
|
||||||
|
await page.mouse.move(793, 489);
|
||||||
|
await page.waitForTimeout(6);
|
||||||
|
await page.mouse.move(794, 489);
|
||||||
|
await page.waitForTimeout(4.1000000005587935);
|
||||||
|
await page.mouse.move(795, 489);
|
||||||
|
await page.waitForTimeout(7.599999999627471);
|
||||||
|
await page.mouse.move(796, 489);
|
||||||
|
await page.waitForTimeout(3.400000000372529);
|
||||||
|
await page.mouse.move(797, 489);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(798, 489);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(799, 489);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(800, 489);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(801, 489);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(802, 489);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(803, 489);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(804, 489);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(805, 489);
|
||||||
|
await page.waitForTimeout(5.8999999994412065);
|
||||||
|
await page.mouse.move(806, 490);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(807, 490);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(809, 490);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(810, 490);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(812, 490);
|
||||||
|
await page.waitForTimeout(1.8999999994412065);
|
||||||
|
await page.mouse.move(813, 490);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(815, 490);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(816, 492);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(818, 492);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(819, 492);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(820, 492);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(822, 492);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(823, 492);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(825, 492);
|
||||||
|
await page.waitForTimeout(2.8999999994412065);
|
||||||
|
await page.mouse.move(826, 492);
|
||||||
|
await page.waitForTimeout(1.1000000005587935);
|
||||||
|
await page.mouse.move(827, 492);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(828, 492);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(830, 492);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(831, 492);
|
||||||
|
await page.waitForTimeout(2.1000000005587935);
|
||||||
|
await page.mouse.move(832, 492);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(833, 493);
|
||||||
|
await page.waitForTimeout(6.1000000005587935);
|
||||||
|
await page.mouse.move(835, 493);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(836, 493);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(838, 493);
|
||||||
|
await page.waitForTimeout(0.8999999994412065);
|
||||||
|
await page.mouse.move(839, 493);
|
||||||
|
await page.waitForTimeout(4.1000000005587935);
|
||||||
|
await page.mouse.move(841, 493);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(842, 493);
|
||||||
|
await page.waitForTimeout(3.8999999994412065);
|
||||||
|
await page.mouse.move(843, 493);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(843, 492);
|
||||||
|
await page.waitForTimeout(4.1000000005587935);
|
||||||
|
await page.mouse.move(844, 492);
|
||||||
|
await page.waitForTimeout(4);
|
||||||
|
await page.mouse.move(844, 493);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(845, 493);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(846, 493);
|
||||||
|
await page.waitForTimeout(4);
|
||||||
|
await page.mouse.move(847, 494);
|
||||||
|
await page.waitForTimeout(24.899999999441206);
|
||||||
|
await page.mouse.move(847, 495);
|
||||||
|
await page.waitForTimeout(29.100000000558794);
|
||||||
|
await page.mouse.move(848, 495);
|
||||||
|
await page.waitForTimeout(297.8999999994412);
|
||||||
|
await page.mouse.move(848, 495);
|
||||||
|
await page.waitForTimeout(5.7000000001862645);
|
||||||
|
await page.mouse.up({ button: "left" });
|
||||||
|
await expect(page).toHaveScreenshot({
|
||||||
|
maxDiffPixels: 100,
|
||||||
|
maxDiffPixelRatio: 0.01,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(11.299999999813735);
|
||||||
|
await page.mouse.move(847, 495);
|
||||||
|
await page.waitForTimeout(199.1000000005588);
|
||||||
|
await page.mouse.move(847, 495);
|
||||||
|
await page.waitForTimeout(249.29999999981374);
|
||||||
|
await page.mouse.move(846, 495);
|
||||||
|
await page.waitForTimeout(5.7000000001862645);
|
||||||
|
await page.mouse.move(846, 496);
|
||||||
|
await page.waitForTimeout(6);
|
||||||
|
await page.mouse.move(845, 496);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(845, 497);
|
||||||
|
await page.waitForTimeout(9);
|
||||||
|
await page.mouse.move(845, 498);
|
||||||
|
await page.waitForTimeout(3.8999999994412065);
|
||||||
|
await page.mouse.move(844, 498);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(844, 500);
|
||||||
|
await page.waitForTimeout(8);
|
||||||
|
await page.mouse.move(844, 501);
|
||||||
|
await page.waitForTimeout(3.1000000005587935);
|
||||||
|
await page.mouse.move(843, 501);
|
||||||
|
await page.waitForTimeout(137);
|
||||||
|
await page.mouse.move(844, 501);
|
||||||
|
await page.waitForTimeout(68.09999999962747);
|
||||||
|
await page.mouse.move(845, 501);
|
||||||
|
await page.waitForTimeout(7.7999999998137355);
|
||||||
|
await page.mouse.move(845, 500);
|
||||||
|
await page.waitForTimeout(1);
|
||||||
|
await page.mouse.move(846, 500);
|
||||||
|
await page.waitForTimeout(4);
|
||||||
|
await page.mouse.move(847, 500);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(848, 500);
|
||||||
|
await page.waitForTimeout(3.1000000005587935);
|
||||||
|
await page.mouse.move(849, 500);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(850, 500);
|
||||||
|
await page.waitForTimeout(5);
|
||||||
|
await page.mouse.move(851, 500);
|
||||||
|
await page.waitForTimeout(2);
|
||||||
|
await page.mouse.move(852, 499);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(854, 499);
|
||||||
|
await page.waitForTimeout(5);
|
||||||
|
await page.mouse.move(855, 499);
|
||||||
|
await page.waitForTimeout(3);
|
||||||
|
await page.mouse.move(856, 499);
|
||||||
|
await page.waitForTimeout(6);
|
||||||
|
await page.mouse.move(857, 499);
|
||||||
|
await page.waitForTimeout(12);
|
||||||
|
await page.mouse.move(858, 499);
|
||||||
|
await page.waitForTimeout(24);
|
||||||
|
await page.mouse.move(858, 498);
|
||||||
|
await page.waitForTimeout(4);
|
||||||
|
await page.mouse.move(858, 498);
|
||||||
|
await page.waitForTimeout(59.200000000186265);
|
||||||
|
await page.keyboard.down("Control");
|
||||||
|
});
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
@@ -11,9 +11,11 @@
|
|||||||
"@babel/preset-env": "7.26.9",
|
"@babel/preset-env": "7.26.9",
|
||||||
"@excalidraw/eslint-config": "1.0.3",
|
"@excalidraw/eslint-config": "1.0.3",
|
||||||
"@excalidraw/prettier-config": "1.0.2",
|
"@excalidraw/prettier-config": "1.0.2",
|
||||||
|
"@playwright/test": "1.55.0",
|
||||||
"@types/chai": "4.3.0",
|
"@types/chai": "4.3.0",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/lodash.throttle": "4.1.7",
|
"@types/lodash.throttle": "4.1.7",
|
||||||
|
"@types/node": "24.3.0",
|
||||||
"@types/react": "19.0.10",
|
"@types/react": "19.0.10",
|
||||||
"@types/react-dom": "19.0.4",
|
"@types/react-dom": "19.0.4",
|
||||||
"@types/socket.io-client": "3.0.0",
|
"@types/socket.io-client": "3.0.0",
|
||||||
@@ -62,6 +64,7 @@
|
|||||||
"build:preview": "yarn --cwd ./excalidraw-app build:preview",
|
"build:preview": "yarn --cwd ./excalidraw-app build:preview",
|
||||||
"start": "yarn --cwd ./excalidraw-app start",
|
"start": "yarn --cwd ./excalidraw-app start",
|
||||||
"start:production": "yarn --cwd ./excalidraw-app start:production",
|
"start:production": "yarn --cwd ./excalidraw-app start:production",
|
||||||
|
"start:test": "yarn --cwd ./excalidraw-app start:test",
|
||||||
"start:example": "yarn build:packages && yarn --cwd ./examples/with-script-in-browser start",
|
"start:example": "yarn build:packages && yarn --cwd ./examples/with-script-in-browser start",
|
||||||
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
|
"test:all": "yarn test:typecheck && yarn test:code && yarn test:other && yarn test:app --watch=false",
|
||||||
"test:app": "vitest",
|
"test:app": "vitest",
|
||||||
|
|||||||
@@ -18,13 +18,22 @@ export const isChrome = navigator.userAgent.indexOf("Chrome") !== -1;
|
|||||||
export const isSafari =
|
export const isSafari =
|
||||||
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
|
!isChrome && navigator.userAgent.indexOf("Safari") !== -1;
|
||||||
export const isIOS =
|
export const isIOS =
|
||||||
/iPad|iPhone/.test(navigator.platform) ||
|
/iPad|iPhone/i.test(navigator.platform) ||
|
||||||
// iPadOS 13+
|
// iPadOS 13+
|
||||||
(navigator.userAgent.includes("Mac") && "ontouchend" in document);
|
(navigator.userAgent.includes("Mac") && "ontouchend" in document);
|
||||||
// keeping function so it can be mocked in test
|
// keeping function so it can be mocked in test
|
||||||
export const isBrave = () =>
|
export const isBrave = () =>
|
||||||
(navigator as any).brave?.isBrave?.name === "isBrave";
|
(navigator as any).brave?.isBrave?.name === "isBrave";
|
||||||
|
|
||||||
|
export const isMobile =
|
||||||
|
isIOS ||
|
||||||
|
/android|webos|ipod|blackberry|iemobile|opera mini/i.test(
|
||||||
|
navigator.userAgent.toLowerCase(),
|
||||||
|
) ||
|
||||||
|
/android|ios|ipod|blackberry|windows phone/i.test(
|
||||||
|
navigator.platform.toLowerCase(),
|
||||||
|
);
|
||||||
|
|
||||||
export const supportsResizeObserver =
|
export const supportsResizeObserver =
|
||||||
typeof window !== "undefined" && "ResizeObserver" in window;
|
typeof window !== "undefined" && "ResizeObserver" in window;
|
||||||
|
|
||||||
|
|||||||
@@ -164,9 +164,14 @@ export class Scene {
|
|||||||
return this.frames;
|
return this.frames;
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(elements: ElementsMapOrArray | null = null) {
|
constructor(
|
||||||
|
elements: ElementsMapOrArray | null = null,
|
||||||
|
options?: {
|
||||||
|
skipValidation?: true;
|
||||||
|
},
|
||||||
|
) {
|
||||||
if (elements) {
|
if (elements) {
|
||||||
this.replaceAllElements(elements);
|
this.replaceAllElements(elements, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,12 +268,19 @@ export class Scene {
|
|||||||
return didChange;
|
return didChange;
|
||||||
}
|
}
|
||||||
|
|
||||||
replaceAllElements(nextElements: ElementsMapOrArray) {
|
replaceAllElements(
|
||||||
|
nextElements: ElementsMapOrArray,
|
||||||
|
options?: {
|
||||||
|
skipValidation?: true;
|
||||||
|
},
|
||||||
|
) {
|
||||||
// we do trust the insertion order on the map, though maybe we shouldn't and should prefer order defined by fractional indices
|
// we do trust the insertion order on the map, though maybe we shouldn't and should prefer order defined by fractional indices
|
||||||
const _nextElements = toArray(nextElements);
|
const _nextElements = toArray(nextElements);
|
||||||
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
|
||||||
|
|
||||||
validateIndicesThrottled(_nextElements);
|
if (!options?.skipValidation) {
|
||||||
|
validateIndicesThrottled(_nextElements);
|
||||||
|
}
|
||||||
|
|
||||||
this.elements = syncInvalidIndices(_nextElements);
|
this.elements = syncInvalidIndices(_nextElements);
|
||||||
this.elementsMap.clear();
|
this.elementsMap.clear();
|
||||||
|
|||||||
+377
-147
@@ -2,7 +2,6 @@ import {
|
|||||||
arrayToMap,
|
arrayToMap,
|
||||||
arrayToObject,
|
arrayToObject,
|
||||||
assertNever,
|
assertNever,
|
||||||
invariant,
|
|
||||||
isDevEnv,
|
isDevEnv,
|
||||||
isShallowEqual,
|
isShallowEqual,
|
||||||
isTestEnv,
|
isTestEnv,
|
||||||
@@ -56,10 +55,10 @@ import { getNonDeletedGroupIds } from "./groups";
|
|||||||
|
|
||||||
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
|
||||||
|
|
||||||
import { Scene } from "./Scene";
|
|
||||||
|
|
||||||
import { StoreSnapshot } from "./store";
|
import { StoreSnapshot } from "./store";
|
||||||
|
|
||||||
|
import { Scene } from "./Scene";
|
||||||
|
|
||||||
import type { BindableProp, BindingProp } from "./binding";
|
import type { BindableProp, BindingProp } from "./binding";
|
||||||
|
|
||||||
import type { ElementUpdate } from "./mutateElement";
|
import type { ElementUpdate } from "./mutateElement";
|
||||||
@@ -151,13 +150,27 @@ export class Delta<T> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges two deltas into a new one.
|
||||||
|
*/
|
||||||
|
public static merge<T>(
|
||||||
|
delta1: Delta<T>,
|
||||||
|
delta2: Delta<T>,
|
||||||
|
delta3: Delta<T> = Delta.empty(),
|
||||||
|
) {
|
||||||
|
return Delta.create(
|
||||||
|
{ ...delta1.deleted, ...delta2.deleted, ...delta3.deleted },
|
||||||
|
{ ...delta1.inserted, ...delta2.inserted, ...delta3.inserted },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merges deleted and inserted object partials.
|
* Merges deleted and inserted object partials.
|
||||||
*/
|
*/
|
||||||
public static mergeObjects<T extends { [key: string]: unknown }>(
|
public static mergeObjects<T extends { [key: string]: unknown }>(
|
||||||
prev: T,
|
prev: T,
|
||||||
added: T,
|
added: T,
|
||||||
removed: T,
|
removed: T = {} as T,
|
||||||
) {
|
) {
|
||||||
const cloned = { ...prev };
|
const cloned = { ...prev };
|
||||||
|
|
||||||
@@ -497,6 +510,11 @@ export interface DeltaContainer<T> {
|
|||||||
*/
|
*/
|
||||||
applyTo(previous: T, ...options: unknown[]): [T, boolean];
|
applyTo(previous: T, ...options: unknown[]): [T, boolean];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Squashes the current delta with the given one.
|
||||||
|
*/
|
||||||
|
squash(delta: DeltaContainer<T>): this;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether all `Delta`s are empty.
|
* Checks whether all `Delta`s are empty.
|
||||||
*/
|
*/
|
||||||
@@ -504,7 +522,11 @@ export interface DeltaContainer<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class AppStateDelta implements DeltaContainer<AppState> {
|
export class AppStateDelta implements DeltaContainer<AppState> {
|
||||||
private constructor(public readonly delta: Delta<ObservedAppState>) {}
|
private constructor(public delta: Delta<ObservedAppState>) {}
|
||||||
|
|
||||||
|
public static create(delta: Delta<ObservedAppState>): AppStateDelta {
|
||||||
|
return new AppStateDelta(delta);
|
||||||
|
}
|
||||||
|
|
||||||
public static calculate<T extends ObservedAppState>(
|
public static calculate<T extends ObservedAppState>(
|
||||||
prevAppState: T,
|
prevAppState: T,
|
||||||
@@ -535,76 +557,137 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
return new AppStateDelta(inversedDelta);
|
return new AppStateDelta(inversedDelta);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public squash(delta: AppStateDelta): this {
|
||||||
|
if (delta.isEmpty()) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mergedDeletedSelectedElementIds = Delta.mergeObjects(
|
||||||
|
this.delta.deleted.selectedElementIds ?? {},
|
||||||
|
delta.delta.deleted.selectedElementIds ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedInsertedSelectedElementIds = Delta.mergeObjects(
|
||||||
|
this.delta.inserted.selectedElementIds ?? {},
|
||||||
|
delta.delta.inserted.selectedElementIds ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedDeletedSelectedGroupIds = Delta.mergeObjects(
|
||||||
|
this.delta.deleted.selectedGroupIds ?? {},
|
||||||
|
delta.delta.deleted.selectedGroupIds ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedInsertedSelectedGroupIds = Delta.mergeObjects(
|
||||||
|
this.delta.inserted.selectedGroupIds ?? {},
|
||||||
|
delta.delta.inserted.selectedGroupIds ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedDeletedLockedMultiSelections = Delta.mergeObjects(
|
||||||
|
this.delta.deleted.lockedMultiSelections ?? {},
|
||||||
|
delta.delta.deleted.lockedMultiSelections ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedInsertedLockedMultiSelections = Delta.mergeObjects(
|
||||||
|
this.delta.inserted.lockedMultiSelections ?? {},
|
||||||
|
delta.delta.inserted.lockedMultiSelections ?? {},
|
||||||
|
);
|
||||||
|
|
||||||
|
const mergedInserted: Partial<ObservedAppState> = {};
|
||||||
|
const mergedDeleted: Partial<ObservedAppState> = {};
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(mergedDeletedSelectedElementIds).length ||
|
||||||
|
Object.keys(mergedInsertedSelectedElementIds).length
|
||||||
|
) {
|
||||||
|
mergedDeleted.selectedElementIds = mergedDeletedSelectedElementIds;
|
||||||
|
mergedInserted.selectedElementIds = mergedInsertedSelectedElementIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(mergedDeletedSelectedGroupIds).length ||
|
||||||
|
Object.keys(mergedInsertedSelectedGroupIds).length
|
||||||
|
) {
|
||||||
|
mergedDeleted.selectedGroupIds = mergedDeletedSelectedGroupIds;
|
||||||
|
mergedInserted.selectedGroupIds = mergedInsertedSelectedGroupIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
Object.keys(mergedDeletedLockedMultiSelections).length ||
|
||||||
|
Object.keys(mergedInsertedLockedMultiSelections).length
|
||||||
|
) {
|
||||||
|
mergedDeleted.lockedMultiSelections = mergedDeletedLockedMultiSelections;
|
||||||
|
mergedInserted.lockedMultiSelections =
|
||||||
|
mergedInsertedLockedMultiSelections;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.delta = Delta.merge(
|
||||||
|
this.delta,
|
||||||
|
delta.delta,
|
||||||
|
Delta.create(mergedDeleted, mergedInserted),
|
||||||
|
);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public applyTo(
|
public applyTo(
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
nextElements: SceneElementsMap,
|
nextElements: SceneElementsMap,
|
||||||
): [AppState, boolean] {
|
): [AppState, boolean] {
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
selectedElementIds: removedSelectedElementIds = {},
|
selectedElementIds: deletedSelectedElementIds = {},
|
||||||
selectedGroupIds: removedSelectedGroupIds = {},
|
selectedGroupIds: deletedSelectedGroupIds = {},
|
||||||
|
lockedMultiSelections: deletedLockedMultiSelections = {},
|
||||||
} = this.delta.deleted;
|
} = this.delta.deleted;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedElementIds: addedSelectedElementIds = {},
|
selectedElementIds: insertedSelectedElementIds = {},
|
||||||
selectedGroupIds: addedSelectedGroupIds = {},
|
selectedGroupIds: insertedSelectedGroupIds = {},
|
||||||
selectedLinearElementId,
|
lockedMultiSelections: insertedLockedMultiSelections = {},
|
||||||
selectedLinearElementIsEditing,
|
selectedLinearElement: insertedSelectedLinearElement,
|
||||||
...directlyApplicablePartial
|
...directlyApplicablePartial
|
||||||
} = this.delta.inserted;
|
} = this.delta.inserted;
|
||||||
|
|
||||||
const mergedSelectedElementIds = Delta.mergeObjects(
|
const mergedSelectedElementIds = Delta.mergeObjects(
|
||||||
appState.selectedElementIds,
|
appState.selectedElementIds,
|
||||||
addedSelectedElementIds,
|
insertedSelectedElementIds,
|
||||||
removedSelectedElementIds,
|
deletedSelectedElementIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergedSelectedGroupIds = Delta.mergeObjects(
|
const mergedSelectedGroupIds = Delta.mergeObjects(
|
||||||
appState.selectedGroupIds,
|
appState.selectedGroupIds,
|
||||||
addedSelectedGroupIds,
|
insertedSelectedGroupIds,
|
||||||
removedSelectedGroupIds,
|
deletedSelectedGroupIds,
|
||||||
);
|
);
|
||||||
|
|
||||||
let selectedLinearElement = appState.selectedLinearElement;
|
const mergedLockedMultiSelections = Delta.mergeObjects(
|
||||||
|
appState.lockedMultiSelections,
|
||||||
|
insertedLockedMultiSelections,
|
||||||
|
deletedLockedMultiSelections,
|
||||||
|
);
|
||||||
|
|
||||||
if (selectedLinearElementId === null) {
|
const selectedLinearElement =
|
||||||
// Unselect linear element (visible change)
|
insertedSelectedLinearElement &&
|
||||||
selectedLinearElement = null;
|
nextElements.has(insertedSelectedLinearElement.elementId)
|
||||||
} else if (
|
? new LinearElementEditor(
|
||||||
selectedLinearElementId &&
|
nextElements.get(
|
||||||
nextElements.has(selectedLinearElementId)
|
insertedSelectedLinearElement.elementId,
|
||||||
) {
|
) as NonDeleted<ExcalidrawLinearElement>,
|
||||||
selectedLinearElement = new LinearElementEditor(
|
nextElements,
|
||||||
nextElements.get(
|
insertedSelectedLinearElement.isEditing,
|
||||||
selectedLinearElementId,
|
)
|
||||||
) as NonDeleted<ExcalidrawLinearElement>,
|
: null;
|
||||||
nextElements,
|
|
||||||
selectedLinearElementIsEditing === true, // Can be unknown which is defaulted to false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
// Value being 'null' is equivaluent to unknown in this case because it only gets set
|
|
||||||
// to null when 'selectedLinearElementId' is set to null
|
|
||||||
selectedLinearElementIsEditing != null
|
|
||||||
) {
|
|
||||||
invariant(
|
|
||||||
selectedLinearElement,
|
|
||||||
`selectedLinearElement is null when selectedLinearElementIsEditing is set to ${selectedLinearElementIsEditing}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
selectedLinearElement = {
|
|
||||||
...selectedLinearElement,
|
|
||||||
isEditing: selectedLinearElementIsEditing,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextAppState = {
|
const nextAppState = {
|
||||||
...appState,
|
...appState,
|
||||||
...directlyApplicablePartial,
|
...directlyApplicablePartial,
|
||||||
selectedElementIds: mergedSelectedElementIds,
|
selectedElementIds: mergedSelectedElementIds,
|
||||||
selectedGroupIds: mergedSelectedGroupIds,
|
selectedGroupIds: mergedSelectedGroupIds,
|
||||||
selectedLinearElement,
|
lockedMultiSelections: mergedLockedMultiSelections,
|
||||||
|
selectedLinearElement:
|
||||||
|
typeof insertedSelectedLinearElement !== "undefined"
|
||||||
|
? selectedLinearElement
|
||||||
|
: appState.selectedLinearElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
const constainsVisibleChanges = this.filterInvisibleChanges(
|
const constainsVisibleChanges = this.filterInvisibleChanges(
|
||||||
@@ -733,64 +816,53 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "selectedLinearElementId": {
|
case "selectedLinearElement":
|
||||||
const appStateKey = AppStateDelta.convertToAppStateKey(key);
|
const nextLinearElement = nextAppState[key];
|
||||||
const linearElement = nextAppState[appStateKey];
|
|
||||||
|
|
||||||
if (!linearElement) {
|
if (!nextLinearElement) {
|
||||||
// previously there was a linear element (assuming visible), now there is none
|
// previously there was a linear element (assuming visible), now there is none
|
||||||
visibleDifferenceFlag.value = true;
|
visibleDifferenceFlag.value = true;
|
||||||
} else {
|
} else {
|
||||||
const element = nextElements.get(linearElement.elementId);
|
const element = nextElements.get(nextLinearElement.elementId);
|
||||||
|
|
||||||
if (element && !element.isDeleted) {
|
if (element && !element.isDeleted) {
|
||||||
// previously there wasn't a linear element, now there is one which is visible
|
// previously there wasn't a linear element, now there is one which is visible
|
||||||
visibleDifferenceFlag.value = true;
|
visibleDifferenceFlag.value = true;
|
||||||
} else {
|
} else {
|
||||||
// there was assigned a linear element now, but it's deleted
|
// there was assigned a linear element now, but it's deleted
|
||||||
nextAppState[appStateKey] = null;
|
nextAppState[key] = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
case "lockedMultiSelections":
|
||||||
case "selectedLinearElementIsEditing": {
|
|
||||||
// Changes in editing state are always visible
|
|
||||||
const prevIsEditing =
|
|
||||||
prevAppState.selectedLinearElement?.isEditing ?? false;
|
|
||||||
const nextIsEditing =
|
|
||||||
nextAppState.selectedLinearElement?.isEditing ?? false;
|
|
||||||
|
|
||||||
if (prevIsEditing !== nextIsEditing) {
|
|
||||||
visibleDifferenceFlag.value = true;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "lockedMultiSelections": {
|
|
||||||
const prevLockedUnits = prevAppState[key] || {};
|
const prevLockedUnits = prevAppState[key] || {};
|
||||||
const nextLockedUnits = nextAppState[key] || {};
|
const nextLockedUnits = nextAppState[key] || {};
|
||||||
|
|
||||||
|
// TODO: this seems wrong, we are already doing this comparison generically above,
|
||||||
|
// hence instead we should check whether elements are actually visible,
|
||||||
|
// so that once these changes are applied they actually result in a visible change to the user
|
||||||
if (!isShallowEqual(prevLockedUnits, nextLockedUnits)) {
|
if (!isShallowEqual(prevLockedUnits, nextLockedUnits)) {
|
||||||
visibleDifferenceFlag.value = true;
|
visibleDifferenceFlag.value = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
case "activeLockedId":
|
||||||
case "activeLockedId": {
|
|
||||||
const prevHitLockedId = prevAppState[key] || null;
|
const prevHitLockedId = prevAppState[key] || null;
|
||||||
const nextHitLockedId = nextAppState[key] || null;
|
const nextHitLockedId = nextAppState[key] || null;
|
||||||
|
|
||||||
|
// TODO: this seems wrong, we are already doing this comparison generically above,
|
||||||
|
// hence instead we should check whether elements are actually visible,
|
||||||
|
// so that once these changes are applied they actually result in a visible change to the user
|
||||||
if (prevHitLockedId !== nextHitLockedId) {
|
if (prevHitLockedId !== nextHitLockedId) {
|
||||||
visibleDifferenceFlag.value = true;
|
visibleDifferenceFlag.value = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
default:
|
||||||
default: {
|
|
||||||
assertNever(
|
assertNever(
|
||||||
key,
|
key,
|
||||||
`Unknown ObservedElementsAppState's key "${key}"`,
|
`Unknown ObservedElementsAppState's key "${key}"`,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -798,15 +870,6 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
return visibleDifferenceFlag.value;
|
return visibleDifferenceFlag.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static convertToAppStateKey(
|
|
||||||
key: keyof Pick<ObservedElementsAppState, "selectedLinearElementId">,
|
|
||||||
): keyof Pick<AppState, "selectedLinearElement"> {
|
|
||||||
switch (key) {
|
|
||||||
case "selectedLinearElementId":
|
|
||||||
return "selectedLinearElement";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static filterSelectedElements(
|
private static filterSelectedElements(
|
||||||
selectedElementIds: AppState["selectedElementIds"],
|
selectedElementIds: AppState["selectedElementIds"],
|
||||||
elements: SceneElementsMap,
|
elements: SceneElementsMap,
|
||||||
@@ -871,8 +934,7 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
editingGroupId,
|
editingGroupId,
|
||||||
selectedGroupIds,
|
selectedGroupIds,
|
||||||
selectedElementIds,
|
selectedElementIds,
|
||||||
selectedLinearElementId,
|
selectedLinearElement,
|
||||||
selectedLinearElementIsEditing,
|
|
||||||
croppingElementId,
|
croppingElementId,
|
||||||
lockedMultiSelections,
|
lockedMultiSelections,
|
||||||
activeLockedId,
|
activeLockedId,
|
||||||
@@ -926,12 +988,6 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
|||||||
"lockedMultiSelections",
|
"lockedMultiSelections",
|
||||||
(prevValue) => (prevValue ?? {}) as ValueOf<T["lockedMultiSelections"]>,
|
(prevValue) => (prevValue ?? {}) as ValueOf<T["lockedMultiSelections"]>,
|
||||||
);
|
);
|
||||||
Delta.diffObjects(
|
|
||||||
deleted,
|
|
||||||
inserted,
|
|
||||||
"activeLockedId",
|
|
||||||
(prevValue) => (prevValue ?? null) as ValueOf<T["activeLockedId"]>,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// if postprocessing fails it does not make sense to bubble up, but let's make sure we know about it
|
// if postprocessing fails it does not make sense to bubble up, but let's make sure we know about it
|
||||||
console.error(`Couldn't postprocess appstate change deltas.`);
|
console.error(`Couldn't postprocess appstate change deltas.`);
|
||||||
@@ -960,12 +1016,13 @@ type ElementPartial<TElement extends ExcalidrawElement = ExcalidrawElement> =
|
|||||||
Omit<Partial<Ordered<TElement>>, "id" | "updated" | "seed">;
|
Omit<Partial<Ordered<TElement>>, "id" | "updated" | "seed">;
|
||||||
|
|
||||||
export type ApplyToOptions = {
|
export type ApplyToOptions = {
|
||||||
excludedProperties: Set<keyof ElementPartial>;
|
excludedProperties?: Set<keyof ElementPartial>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ApplyToFlags = {
|
type ApplyToFlags = {
|
||||||
containsVisibleDifference: boolean;
|
containsVisibleDifference: boolean;
|
||||||
containsZindexDifference: boolean;
|
containsZindexDifference: boolean;
|
||||||
|
applyDirection: "forward" | "backward" | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1066,6 +1123,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
deleted.version !== inserted.version
|
deleted.version !== inserted.version
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static satisfiesUniqueInvariants = (
|
||||||
|
elementsDelta: ElementsDelta,
|
||||||
|
id: string,
|
||||||
|
) => {
|
||||||
|
const { added, removed, updated } = elementsDelta;
|
||||||
|
// it's required that there is only one unique delta type per element
|
||||||
|
return [added[id], removed[id], updated[id]].filter(Boolean).length === 1;
|
||||||
|
};
|
||||||
|
|
||||||
private static validate(
|
private static validate(
|
||||||
elementsDelta: ElementsDelta,
|
elementsDelta: ElementsDelta,
|
||||||
type: "added" | "removed" | "updated",
|
type: "added" | "removed" | "updated",
|
||||||
@@ -1074,6 +1140,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
for (const [id, delta] of Object.entries(elementsDelta[type])) {
|
for (const [id, delta] of Object.entries(elementsDelta[type])) {
|
||||||
if (
|
if (
|
||||||
!this.satisfiesCommmonInvariants(delta) ||
|
!this.satisfiesCommmonInvariants(delta) ||
|
||||||
|
!this.satisfiesUniqueInvariants(elementsDelta, id) ||
|
||||||
!satifiesSpecialInvariants(delta)
|
!satifiesSpecialInvariants(delta)
|
||||||
) {
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
@@ -1110,7 +1177,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const nextElement = nextElements.get(prevElement.id);
|
const nextElement = nextElements.get(prevElement.id);
|
||||||
|
|
||||||
if (!nextElement) {
|
if (!nextElement) {
|
||||||
const deleted = { ...prevElement, isDeleted: false } as ElementPartial;
|
const deleted = { ...prevElement } as ElementPartial;
|
||||||
|
|
||||||
const inserted = {
|
const inserted = {
|
||||||
isDeleted: true,
|
isDeleted: true,
|
||||||
@@ -1124,7 +1191,10 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
ElementsDelta.stripIrrelevantProps,
|
ElementsDelta.stripIrrelevantProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
removed[prevElement.id] = delta;
|
// ignore updates which would "delete" already deleted element
|
||||||
|
if (!prevElement.isDeleted) {
|
||||||
|
removed[prevElement.id] = delta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1140,7 +1210,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
|
|
||||||
const inserted = {
|
const inserted = {
|
||||||
...nextElement,
|
...nextElement,
|
||||||
isDeleted: false,
|
|
||||||
} as ElementPartial;
|
} as ElementPartial;
|
||||||
|
|
||||||
const delta = Delta.create(
|
const delta = Delta.create(
|
||||||
@@ -1149,7 +1218,10 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
ElementsDelta.stripIrrelevantProps,
|
ElementsDelta.stripIrrelevantProps,
|
||||||
);
|
);
|
||||||
|
|
||||||
added[nextElement.id] = delta;
|
// ignore updates which would "delete" already deleted element
|
||||||
|
if (!nextElement.isDeleted) {
|
||||||
|
added[nextElement.id] = delta;
|
||||||
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1178,8 +1250,13 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// making sure there are at least some changes
|
const strippedDeleted = ElementsDelta.stripVersionProps(delta.deleted);
|
||||||
if (!Delta.isEmpty(delta)) {
|
const strippedInserted = ElementsDelta.stripVersionProps(
|
||||||
|
delta.inserted,
|
||||||
|
);
|
||||||
|
|
||||||
|
// making sure there are at least some changes and only changed version & versionNonce does not count!
|
||||||
|
if (Delta.isInnerDifferent(strippedDeleted, strippedInserted, true)) {
|
||||||
updated[nextElement.id] = delta;
|
updated[nextElement.id] = delta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1196,8 +1273,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const inverseInternal = (deltas: Record<string, Delta<ElementPartial>>) => {
|
const inverseInternal = (deltas: Record<string, Delta<ElementPartial>>) => {
|
||||||
const inversedDeltas: Record<string, Delta<ElementPartial>> = {};
|
const inversedDeltas: Record<string, Delta<ElementPartial>> = {};
|
||||||
|
|
||||||
for (const [id, delta] of Object.entries(deltas)) {
|
for (const [id, { inserted, deleted }] of Object.entries(deltas)) {
|
||||||
inversedDeltas[id] = Delta.create(delta.inserted, delta.deleted);
|
inversedDeltas[id] = Delta.create({ ...inserted }, { ...deleted });
|
||||||
}
|
}
|
||||||
|
|
||||||
return inversedDeltas;
|
return inversedDeltas;
|
||||||
@@ -1295,8 +1372,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
latestDelta = delta;
|
latestDelta = delta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const strippedDeleted = ElementsDelta.stripVersionProps(
|
||||||
|
latestDelta.deleted,
|
||||||
|
);
|
||||||
|
const strippedInserted = ElementsDelta.stripVersionProps(
|
||||||
|
latestDelta.inserted,
|
||||||
|
);
|
||||||
|
|
||||||
// it might happen that after applying latest changes the delta itself does not contain any changes
|
// it might happen that after applying latest changes the delta itself does not contain any changes
|
||||||
if (Delta.isInnerDifferent(latestDelta.deleted, latestDelta.inserted)) {
|
if (Delta.isInnerDifferent(strippedDeleted, strippedInserted)) {
|
||||||
modifiedDeltas[id] = latestDelta;
|
modifiedDeltas[id] = latestDelta;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1316,9 +1400,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
public applyTo(
|
public applyTo(
|
||||||
elements: SceneElementsMap,
|
elements: SceneElementsMap,
|
||||||
snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements,
|
snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements,
|
||||||
options: ApplyToOptions = {
|
options?: ApplyToOptions,
|
||||||
excludedProperties: new Set(),
|
|
||||||
},
|
|
||||||
): [SceneElementsMap, boolean] {
|
): [SceneElementsMap, boolean] {
|
||||||
let nextElements = new Map(elements) as SceneElementsMap;
|
let nextElements = new Map(elements) as SceneElementsMap;
|
||||||
let changedElements: Map<string, OrderedExcalidrawElement>;
|
let changedElements: Map<string, OrderedExcalidrawElement>;
|
||||||
@@ -1326,22 +1408,28 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const flags: ApplyToFlags = {
|
const flags: ApplyToFlags = {
|
||||||
containsVisibleDifference: false,
|
containsVisibleDifference: false,
|
||||||
containsZindexDifference: false,
|
containsZindexDifference: false,
|
||||||
|
applyDirection: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// mimic a transaction by applying deltas into `nextElements` (always new instance, no mutation)
|
// mimic a transaction by applying deltas into `nextElements` (always new instance, no mutation)
|
||||||
try {
|
try {
|
||||||
const applyDeltas = ElementsDelta.createApplier(
|
const applyDeltas = ElementsDelta.createApplier(
|
||||||
|
elements,
|
||||||
nextElements,
|
nextElements,
|
||||||
snapshot,
|
snapshot,
|
||||||
options,
|
|
||||||
flags,
|
flags,
|
||||||
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
const addedElements = applyDeltas(this.added);
|
const addedElements = applyDeltas(this.added);
|
||||||
const removedElements = applyDeltas(this.removed);
|
const removedElements = applyDeltas(this.removed);
|
||||||
const updatedElements = applyDeltas(this.updated);
|
const updatedElements = applyDeltas(this.updated);
|
||||||
|
|
||||||
const affectedElements = this.resolveConflicts(elements, nextElements);
|
const affectedElements = this.resolveConflicts(
|
||||||
|
elements,
|
||||||
|
nextElements,
|
||||||
|
flags.applyDirection,
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: #7348 validate elements semantically and syntactically the changed elements, in case they would result data integrity issues
|
// TODO: #7348 validate elements semantically and syntactically the changed elements, in case they would result data integrity issues
|
||||||
changedElements = new Map([
|
changedElements = new Map([
|
||||||
@@ -1365,22 +1453,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// the following reorder performs also mutations, but only on new instances of changed elements
|
// the following reorder performs mutations, but only on new instances of changed elements,
|
||||||
// (unless something goes really bad and it fallbacks to fixing all invalid indices)
|
// unless something goes really bad and it fallbacks to fixing all invalid indices
|
||||||
nextElements = ElementsDelta.reorderElements(
|
nextElements = ElementsDelta.reorderElements(
|
||||||
nextElements,
|
nextElements,
|
||||||
changedElements,
|
changedElements,
|
||||||
flags,
|
flags,
|
||||||
);
|
);
|
||||||
|
|
||||||
// we don't have an up-to-date scene, as we can be just in the middle of applying history entry
|
ElementsDelta.redrawElements(nextElements, changedElements);
|
||||||
// we also don't have a scene on the server
|
|
||||||
// so we are creating a temp scene just to query and mutate elements
|
|
||||||
const tempScene = new Scene(nextElements);
|
|
||||||
|
|
||||||
ElementsDelta.redrawTextBoundingBoxes(tempScene, changedElements);
|
|
||||||
// Need ordered nextElements to avoid z-index binding issues
|
|
||||||
ElementsDelta.redrawBoundArrows(tempScene, changedElements);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(
|
console.error(
|
||||||
`Couldn't mutate elements after applying elements change`,
|
`Couldn't mutate elements after applying elements change`,
|
||||||
@@ -1395,12 +1476,113 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public squash(delta: ElementsDelta): this {
|
||||||
|
if (delta.isEmpty()) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { added, removed, updated } = delta;
|
||||||
|
|
||||||
|
const mergeBoundElements = (
|
||||||
|
prevDelta: Delta<ElementPartial>,
|
||||||
|
nextDelta: Delta<ElementPartial>,
|
||||||
|
) => {
|
||||||
|
const mergedDeletedBoundElements =
|
||||||
|
Delta.mergeArrays(
|
||||||
|
prevDelta.deleted.boundElements ?? [],
|
||||||
|
nextDelta.deleted.boundElements ?? [],
|
||||||
|
undefined,
|
||||||
|
(x) => x.id,
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
const mergedInsertedBoundElements =
|
||||||
|
Delta.mergeArrays(
|
||||||
|
prevDelta.inserted.boundElements ?? [],
|
||||||
|
nextDelta.inserted.boundElements ?? [],
|
||||||
|
undefined,
|
||||||
|
(x) => x.id,
|
||||||
|
) ?? [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
!mergedDeletedBoundElements.length &&
|
||||||
|
!mergedInsertedBoundElements.length
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Delta.create(
|
||||||
|
{
|
||||||
|
boundElements: mergedDeletedBoundElements,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
boundElements: mergedInsertedBoundElements,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [id, nextDelta] of Object.entries(added)) {
|
||||||
|
const prevDelta = this.added[id] ?? this.removed[id] ?? this.updated[id];
|
||||||
|
|
||||||
|
if (!prevDelta) {
|
||||||
|
this.added[id] = nextDelta;
|
||||||
|
} else {
|
||||||
|
const mergedDelta = mergeBoundElements(prevDelta, nextDelta);
|
||||||
|
delete this.removed[id];
|
||||||
|
delete this.updated[id];
|
||||||
|
|
||||||
|
this.added[id] = Delta.merge(prevDelta, nextDelta, mergedDelta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, nextDelta] of Object.entries(removed)) {
|
||||||
|
const prevDelta = this.added[id] ?? this.removed[id] ?? this.updated[id];
|
||||||
|
|
||||||
|
if (!prevDelta) {
|
||||||
|
this.removed[id] = nextDelta;
|
||||||
|
} else {
|
||||||
|
const mergedDelta = mergeBoundElements(prevDelta, nextDelta);
|
||||||
|
delete this.added[id];
|
||||||
|
delete this.updated[id];
|
||||||
|
|
||||||
|
this.removed[id] = Delta.merge(prevDelta, nextDelta, mergedDelta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [id, nextDelta] of Object.entries(updated)) {
|
||||||
|
const prevDelta = this.added[id] ?? this.removed[id] ?? this.updated[id];
|
||||||
|
|
||||||
|
if (!prevDelta) {
|
||||||
|
this.updated[id] = nextDelta;
|
||||||
|
} else {
|
||||||
|
const mergedDelta = mergeBoundElements(prevDelta, nextDelta);
|
||||||
|
const updatedDelta = Delta.merge(prevDelta, nextDelta, mergedDelta);
|
||||||
|
|
||||||
|
if (prevDelta === this.added[id]) {
|
||||||
|
this.added[id] = updatedDelta;
|
||||||
|
} else if (prevDelta === this.removed[id]) {
|
||||||
|
this.removed[id] = updatedDelta;
|
||||||
|
} else {
|
||||||
|
this.updated[id] = updatedDelta;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTestEnv() || isDevEnv()) {
|
||||||
|
ElementsDelta.validate(this, "added", ElementsDelta.satisfiesAddition);
|
||||||
|
ElementsDelta.validate(this, "removed", ElementsDelta.satisfiesRemoval);
|
||||||
|
ElementsDelta.validate(this, "updated", ElementsDelta.satisfiesUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
private static createApplier =
|
private static createApplier =
|
||||||
(
|
(
|
||||||
|
prevElements: SceneElementsMap,
|
||||||
nextElements: SceneElementsMap,
|
nextElements: SceneElementsMap,
|
||||||
snapshot: StoreSnapshot["elements"],
|
snapshot: StoreSnapshot["elements"],
|
||||||
options: ApplyToOptions,
|
|
||||||
flags: ApplyToFlags,
|
flags: ApplyToFlags,
|
||||||
|
options?: ApplyToOptions,
|
||||||
) =>
|
) =>
|
||||||
(deltas: Record<string, Delta<ElementPartial>>) => {
|
(deltas: Record<string, Delta<ElementPartial>>) => {
|
||||||
const getElement = ElementsDelta.createGetter(
|
const getElement = ElementsDelta.createGetter(
|
||||||
@@ -1413,15 +1595,26 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
const element = getElement(id, delta.inserted);
|
const element = getElement(id, delta.inserted);
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
const newElement = ElementsDelta.applyDelta(
|
const nextElement = ElementsDelta.applyDelta(
|
||||||
element,
|
element,
|
||||||
delta,
|
delta,
|
||||||
options,
|
|
||||||
flags,
|
flags,
|
||||||
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
nextElements.set(newElement.id, newElement);
|
nextElements.set(nextElement.id, nextElement);
|
||||||
acc.set(newElement.id, newElement);
|
acc.set(nextElement.id, nextElement);
|
||||||
|
|
||||||
|
if (!flags.applyDirection) {
|
||||||
|
const prevElement = prevElements.get(id);
|
||||||
|
|
||||||
|
if (prevElement) {
|
||||||
|
flags.applyDirection =
|
||||||
|
prevElement.version > nextElement.version
|
||||||
|
? "backward"
|
||||||
|
: "forward";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return acc;
|
return acc;
|
||||||
@@ -1466,8 +1659,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
private static applyDelta(
|
private static applyDelta(
|
||||||
element: OrderedExcalidrawElement,
|
element: OrderedExcalidrawElement,
|
||||||
delta: Delta<ElementPartial>,
|
delta: Delta<ElementPartial>,
|
||||||
options: ApplyToOptions,
|
|
||||||
flags: ApplyToFlags,
|
flags: ApplyToFlags,
|
||||||
|
options?: ApplyToOptions,
|
||||||
) {
|
) {
|
||||||
const directlyApplicablePartial: Mutable<ElementPartial> = {};
|
const directlyApplicablePartial: Mutable<ElementPartial> = {};
|
||||||
|
|
||||||
@@ -1481,7 +1674,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.excludedProperties.has(key)) {
|
if (options?.excludedProperties?.has(key)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1521,7 +1714,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
delta.deleted.index !== delta.inserted.index;
|
delta.deleted.index !== delta.inserted.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newElementWith(element, directlyApplicablePartial);
|
return newElementWith(element, directlyApplicablePartial, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1561,6 +1754,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
private resolveConflicts(
|
private resolveConflicts(
|
||||||
prevElements: SceneElementsMap,
|
prevElements: SceneElementsMap,
|
||||||
nextElements: SceneElementsMap,
|
nextElements: SceneElementsMap,
|
||||||
|
applyDirection: "forward" | "backward" = "forward",
|
||||||
) {
|
) {
|
||||||
const nextAffectedElements = new Map<string, OrderedExcalidrawElement>();
|
const nextAffectedElements = new Map<string, OrderedExcalidrawElement>();
|
||||||
const updater = (
|
const updater = (
|
||||||
@@ -1572,21 +1766,36 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const prevElement = prevElements.get(element.id);
|
||||||
|
const nextVersion =
|
||||||
|
applyDirection === "forward"
|
||||||
|
? nextElement.version + 1
|
||||||
|
: nextElement.version - 1;
|
||||||
|
|
||||||
|
const elementUpdates = updates as ElementUpdate<OrderedExcalidrawElement>;
|
||||||
|
|
||||||
let affectedElement: OrderedExcalidrawElement;
|
let affectedElement: OrderedExcalidrawElement;
|
||||||
|
|
||||||
if (prevElements.get(element.id) === nextElement) {
|
if (prevElement === nextElement) {
|
||||||
// create the new element instance in case we didn't modify the element yet
|
// create the new element instance in case we didn't modify the element yet
|
||||||
// so that we won't end up in an incosistent state in case we would fail in the middle of mutations
|
// so that we won't end up in an incosistent state in case we would fail in the middle of mutations
|
||||||
affectedElement = newElementWith(
|
affectedElement = newElementWith(
|
||||||
nextElement,
|
nextElement,
|
||||||
updates as ElementUpdate<OrderedExcalidrawElement>,
|
{
|
||||||
|
...elementUpdates,
|
||||||
|
version: nextVersion,
|
||||||
|
},
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
affectedElement = mutateElement(
|
affectedElement = mutateElement(nextElement, nextElements, {
|
||||||
nextElement,
|
...elementUpdates,
|
||||||
nextElements,
|
// don't modify the version further, if it's already different
|
||||||
updates as ElementUpdate<OrderedExcalidrawElement>,
|
version:
|
||||||
);
|
prevElement?.version !== nextElement.version
|
||||||
|
? nextElement.version
|
||||||
|
: nextVersion,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
nextAffectedElements.set(affectedElement.id, affectedElement);
|
nextAffectedElements.set(affectedElement.id, affectedElement);
|
||||||
@@ -1624,25 +1833,12 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
Array.from(prevElements).filter(([id]) => nextAffectedElements.has(id)),
|
Array.from(prevElements).filter(([id]) => nextAffectedElements.has(id)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// calculate complete deltas for affected elements, and assign them back to all the deltas
|
// calculate complete deltas for affected elements, and squash them back to the current deltas
|
||||||
// technically we could do better here if perf. would become an issue
|
this.squash(
|
||||||
const { added, removed, updated } = ElementsDelta.calculate(
|
// technically we could do better here if perf. would become an issue
|
||||||
prevAffectedElements,
|
ElementsDelta.calculate(prevAffectedElements, nextAffectedElements),
|
||||||
nextAffectedElements,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
for (const [id, delta] of Object.entries(added)) {
|
|
||||||
this.added[id] = delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [id, delta] of Object.entries(removed)) {
|
|
||||||
this.removed[id] = delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [id, delta] of Object.entries(updated)) {
|
|
||||||
this.updated[id] = delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nextAffectedElements;
|
return nextAffectedElements;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1704,6 +1900,31 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
BindableElement.rebindAffected(nextElements, nextElement(), updater);
|
BindableElement.rebindAffected(nextElements, nextElement(), updater);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static redrawElements(
|
||||||
|
nextElements: SceneElementsMap,
|
||||||
|
changedElements: Map<string, OrderedExcalidrawElement>,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
// we don't have an up-to-date scene, as we can be just in the middle of applying history entry
|
||||||
|
// we also don't have a scene on the server
|
||||||
|
// so we are creating a temp scene just to query and mutate elements
|
||||||
|
const tempScene = new Scene(nextElements, { skipValidation: true });
|
||||||
|
|
||||||
|
ElementsDelta.redrawTextBoundingBoxes(tempScene, changedElements);
|
||||||
|
|
||||||
|
// needs ordered nextElements to avoid z-index binding issues
|
||||||
|
ElementsDelta.redrawBoundArrows(tempScene, changedElements);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Couldn't redraw elements`, e);
|
||||||
|
|
||||||
|
if (isTestEnv() || isDevEnv()) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
return nextElements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static redrawTextBoundingBoxes(
|
private static redrawTextBoundingBoxes(
|
||||||
scene: Scene,
|
scene: Scene,
|
||||||
changed: Map<string, OrderedExcalidrawElement>,
|
changed: Map<string, OrderedExcalidrawElement>,
|
||||||
@@ -1758,6 +1979,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
) {
|
) {
|
||||||
for (const element of changed.values()) {
|
for (const element of changed.values()) {
|
||||||
if (!element.isDeleted && isBindableElement(element)) {
|
if (!element.isDeleted && isBindableElement(element)) {
|
||||||
|
// TODO: with precise bindings this is quite expensive, so consider optimisation so it's only triggered when the arrow does not intersect (imprecise) element bounds
|
||||||
updateBoundElements(element, scene, {
|
updateBoundElements(element, scene, {
|
||||||
changedElements: changed,
|
changedElements: changed,
|
||||||
});
|
});
|
||||||
@@ -1853,4 +2075,12 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
|||||||
|
|
||||||
return strippedPartial;
|
return strippedPartial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static stripVersionProps(
|
||||||
|
partial: Partial<OrderedExcalidrawElement>,
|
||||||
|
): ElementPartial {
|
||||||
|
const { version, versionNonce, ...strippedPartial } = partial;
|
||||||
|
|
||||||
|
return strippedPartial;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
assertNever,
|
assertNever,
|
||||||
COLOR_PALETTE,
|
COLOR_PALETTE,
|
||||||
LINE_POLYGON_POINT_MERGE_DISTANCE,
|
LINE_POLYGON_POINT_MERGE_DISTANCE,
|
||||||
|
isTestEnv,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
import { RoughGenerator } from "roughjs/bin/generator";
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
@@ -182,7 +183,7 @@ export const generateRoughOptions = (
|
|||||||
continuousPath = false,
|
continuousPath = false,
|
||||||
): Options => {
|
): Options => {
|
||||||
const options: Options = {
|
const options: Options = {
|
||||||
seed: element.seed,
|
seed: isTestEnv() ? 1 : element.seed,
|
||||||
strokeLineDash:
|
strokeLineDash:
|
||||||
element.strokeStyle === "dashed"
|
element.strokeStyle === "dashed"
|
||||||
? getDashArrayDashed(element.strokeWidth)
|
? getDashArrayDashed(element.strokeWidth)
|
||||||
|
|||||||
@@ -76,8 +76,9 @@ type MicroActionsQueue = (() => void)[];
|
|||||||
* Store which captures the observed changes and emits them as `StoreIncrement` events.
|
* Store which captures the observed changes and emits them as `StoreIncrement` events.
|
||||||
*/
|
*/
|
||||||
export class Store {
|
export class Store {
|
||||||
// internally used by history
|
// for internal use by history
|
||||||
public readonly onDurableIncrementEmitter = new Emitter<[DurableIncrement]>();
|
public readonly onDurableIncrementEmitter = new Emitter<[DurableIncrement]>();
|
||||||
|
// for public use as part of onIncrement API
|
||||||
public readonly onStoreIncrementEmitter = new Emitter<
|
public readonly onStoreIncrementEmitter = new Emitter<
|
||||||
[DurableIncrement | EphemeralIncrement]
|
[DurableIncrement | EphemeralIncrement]
|
||||||
>();
|
>();
|
||||||
@@ -239,7 +240,6 @@ export class Store {
|
|||||||
if (!storeDelta.isEmpty()) {
|
if (!storeDelta.isEmpty()) {
|
||||||
const increment = new DurableIncrement(storeChange, storeDelta);
|
const increment = new DurableIncrement(storeChange, storeDelta);
|
||||||
|
|
||||||
// Notify listeners with the increment
|
|
||||||
this.onDurableIncrementEmitter.trigger(increment);
|
this.onDurableIncrementEmitter.trigger(increment);
|
||||||
this.onStoreIncrementEmitter.trigger(increment);
|
this.onStoreIncrementEmitter.trigger(increment);
|
||||||
}
|
}
|
||||||
@@ -552,10 +552,26 @@ export class StoreDelta {
|
|||||||
public static load({
|
public static load({
|
||||||
id,
|
id,
|
||||||
elements: { added, removed, updated },
|
elements: { added, removed, updated },
|
||||||
|
appState: { delta: appStateDelta },
|
||||||
}: DTO<StoreDelta>) {
|
}: DTO<StoreDelta>) {
|
||||||
const elements = ElementsDelta.create(added, removed, updated);
|
const elements = ElementsDelta.create(added, removed, updated);
|
||||||
|
const appState = AppStateDelta.create(appStateDelta);
|
||||||
|
|
||||||
return new this(id, elements, AppStateDelta.empty());
|
return new this(id, elements, appState);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Squash the passed deltas into the aggregated delta instance.
|
||||||
|
*/
|
||||||
|
public static squash(...deltas: StoreDelta[]) {
|
||||||
|
const aggregatedDelta = StoreDelta.empty();
|
||||||
|
|
||||||
|
for (const delta of deltas) {
|
||||||
|
aggregatedDelta.elements.squash(delta.elements);
|
||||||
|
aggregatedDelta.appState.squash(delta.appState);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aggregatedDelta;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -572,9 +588,7 @@ export class StoreDelta {
|
|||||||
delta: StoreDelta,
|
delta: StoreDelta,
|
||||||
elements: SceneElementsMap,
|
elements: SceneElementsMap,
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
options: ApplyToOptions = {
|
options?: ApplyToOptions,
|
||||||
excludedProperties: new Set(),
|
|
||||||
},
|
|
||||||
): [SceneElementsMap, AppState, boolean] {
|
): [SceneElementsMap, AppState, boolean] {
|
||||||
const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo(
|
const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo(
|
||||||
elements,
|
elements,
|
||||||
@@ -613,6 +627,10 @@ export class StoreDelta {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static empty() {
|
||||||
|
return StoreDelta.create(ElementsDelta.empty(), AppStateDelta.empty());
|
||||||
|
}
|
||||||
|
|
||||||
public isEmpty() {
|
public isEmpty() {
|
||||||
return this.elements.isEmpty() && this.appState.isEmpty();
|
return this.elements.isEmpty() && this.appState.isEmpty();
|
||||||
}
|
}
|
||||||
@@ -978,8 +996,7 @@ const getDefaultObservedAppState = (): ObservedAppState => {
|
|||||||
viewBackgroundColor: COLOR_PALETTE.white,
|
viewBackgroundColor: COLOR_PALETTE.white,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
selectedLinearElementId: null,
|
selectedLinearElement: null,
|
||||||
selectedLinearElementIsEditing: null,
|
|
||||||
croppingElementId: null,
|
croppingElementId: null,
|
||||||
activeLockedId: null,
|
activeLockedId: null,
|
||||||
lockedMultiSelections: {},
|
lockedMultiSelections: {},
|
||||||
@@ -998,14 +1015,12 @@ export const getObservedAppState = (
|
|||||||
croppingElementId: appState.croppingElementId,
|
croppingElementId: appState.croppingElementId,
|
||||||
activeLockedId: appState.activeLockedId,
|
activeLockedId: appState.activeLockedId,
|
||||||
lockedMultiSelections: appState.lockedMultiSelections,
|
lockedMultiSelections: appState.lockedMultiSelections,
|
||||||
selectedLinearElementId:
|
selectedLinearElement: appState.selectedLinearElement
|
||||||
(appState as AppState).selectedLinearElement?.elementId ??
|
? {
|
||||||
(appState as ObservedAppState).selectedLinearElementId ??
|
elementId: appState.selectedLinearElement.elementId,
|
||||||
null,
|
isEditing: !!appState.selectedLinearElement.isEditing,
|
||||||
selectedLinearElementIsEditing:
|
}
|
||||||
(appState as AppState).selectedLinearElement?.isEditing ??
|
: null,
|
||||||
(appState as ObservedAppState).selectedLinearElementIsEditing ??
|
|
||||||
null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {
|
Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, {
|
||||||
|
|||||||
@@ -1,13 +1,328 @@
|
|||||||
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
|
|
||||||
import type { ObservedAppState } from "@excalidraw/excalidraw/types";
|
import type { ObservedAppState } from "@excalidraw/excalidraw/types";
|
||||||
import type { LinearElementEditor } from "@excalidraw/element";
|
import type { LinearElementEditor } from "@excalidraw/element";
|
||||||
|
import type { SceneElementsMap } from "@excalidraw/element/types";
|
||||||
|
|
||||||
import { AppStateDelta } from "../src/delta";
|
import { AppStateDelta, Delta, ElementsDelta } from "../src/delta";
|
||||||
|
|
||||||
|
describe("ElementsDelta", () => {
|
||||||
|
describe("elements delta calculation", () => {
|
||||||
|
it("should not create removed delta when element gets removed but was already deleted", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
isDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevElements = new Map([[element.id, element]]);
|
||||||
|
const nextElements = new Map();
|
||||||
|
|
||||||
|
const delta = ElementsDelta.calculate(prevElements, nextElements);
|
||||||
|
|
||||||
|
expect(delta.isEmpty()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create added delta when adding element as already deleted", () => {
|
||||||
|
const element = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
isDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const prevElements = new Map();
|
||||||
|
const nextElements = new Map([[element.id, element]]);
|
||||||
|
|
||||||
|
const delta = ElementsDelta.calculate(prevElements, nextElements);
|
||||||
|
|
||||||
|
expect(delta.isEmpty()).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not create updated delta when there is only version and versionNonce change", () => {
|
||||||
|
const baseElement = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
strokeColor: "#000000",
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
});
|
||||||
|
|
||||||
|
const modifiedElement = {
|
||||||
|
...baseElement,
|
||||||
|
version: baseElement.version + 1,
|
||||||
|
versionNonce: baseElement.versionNonce + 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create maps for the delta calculation
|
||||||
|
const prevElements = new Map([[baseElement.id, baseElement]]);
|
||||||
|
const nextElements = new Map([[modifiedElement.id, modifiedElement]]);
|
||||||
|
|
||||||
|
// Calculate the delta
|
||||||
|
const delta = ElementsDelta.calculate(
|
||||||
|
prevElements as SceneElementsMap,
|
||||||
|
nextElements as SceneElementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(delta.isEmpty()).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("squash", () => {
|
||||||
|
it("should not squash when second delta is empty", () => {
|
||||||
|
const updatedDelta = Delta.create(
|
||||||
|
{ x: 100, version: 1, versionNonce: 1 },
|
||||||
|
{ x: 200, version: 2, versionNonce: 2 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsDelta1 = ElementsDelta.create(
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{ id1: updatedDelta },
|
||||||
|
);
|
||||||
|
const elementsDelta2 = ElementsDelta.empty();
|
||||||
|
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
||||||
|
|
||||||
|
expect(elementsDelta.isEmpty()).toBeFalsy();
|
||||||
|
expect(elementsDelta).toBe(elementsDelta1);
|
||||||
|
expect(elementsDelta.updated.id1).toBe(updatedDelta);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should squash mutually exclusive delta types", () => {
|
||||||
|
const addedDelta = Delta.create(
|
||||||
|
{ x: 100, version: 1, versionNonce: 1, isDeleted: true },
|
||||||
|
{ x: 200, version: 2, versionNonce: 2, isDeleted: false },
|
||||||
|
);
|
||||||
|
|
||||||
|
const removedDelta = Delta.create(
|
||||||
|
{ x: 100, version: 1, versionNonce: 1, isDeleted: false },
|
||||||
|
{ x: 200, version: 2, versionNonce: 2, isDeleted: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatedDelta = Delta.create(
|
||||||
|
{ x: 100, version: 1, versionNonce: 1 },
|
||||||
|
{ x: 200, version: 2, versionNonce: 2 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsDelta1 = ElementsDelta.create(
|
||||||
|
{ id1: addedDelta },
|
||||||
|
{ id2: removedDelta },
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsDelta2 = ElementsDelta.create(
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{ id3: updatedDelta },
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
||||||
|
|
||||||
|
expect(elementsDelta.isEmpty()).toBeFalsy();
|
||||||
|
expect(elementsDelta).toBe(elementsDelta1);
|
||||||
|
expect(elementsDelta.added.id1).toBe(addedDelta);
|
||||||
|
expect(elementsDelta.removed.id2).toBe(removedDelta);
|
||||||
|
expect(elementsDelta.updated.id3).toBe(updatedDelta);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should squash the same delta types", () => {
|
||||||
|
const elementsDelta1 = ElementsDelta.create(
|
||||||
|
{
|
||||||
|
id1: Delta.create(
|
||||||
|
{ x: 100, version: 1, versionNonce: 1, isDeleted: true },
|
||||||
|
{ x: 200, version: 2, versionNonce: 2, isDeleted: false },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id2: Delta.create(
|
||||||
|
{ x: 100, version: 1, versionNonce: 1, isDeleted: false },
|
||||||
|
{ x: 200, version: 2, versionNonce: 2, isDeleted: true },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id3: Delta.create(
|
||||||
|
{ x: 100, version: 1, versionNonce: 1 },
|
||||||
|
{ x: 200, version: 2, versionNonce: 2 },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsDelta2 = ElementsDelta.create(
|
||||||
|
{
|
||||||
|
id1: Delta.create(
|
||||||
|
{ y: 100, version: 2, versionNonce: 2, isDeleted: true },
|
||||||
|
{ y: 200, version: 3, versionNonce: 3, isDeleted: false },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id2: Delta.create(
|
||||||
|
{ y: 100, version: 2, versionNonce: 2, isDeleted: false },
|
||||||
|
{ y: 200, version: 3, versionNonce: 3, isDeleted: true },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id3: Delta.create(
|
||||||
|
{ y: 100, version: 2, versionNonce: 2 },
|
||||||
|
{ y: 200, version: 3, versionNonce: 3 },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
||||||
|
|
||||||
|
expect(elementsDelta.isEmpty()).toBeFalsy();
|
||||||
|
expect(elementsDelta).toBe(elementsDelta1);
|
||||||
|
expect(elementsDelta.added.id1).toEqual(
|
||||||
|
Delta.create(
|
||||||
|
{ x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: true },
|
||||||
|
{ x: 200, y: 200, version: 3, versionNonce: 3, isDeleted: false },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(elementsDelta.removed.id2).toEqual(
|
||||||
|
Delta.create(
|
||||||
|
{ x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: false },
|
||||||
|
{ x: 200, y: 200, version: 3, versionNonce: 3, isDeleted: true },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(elementsDelta.updated.id3).toEqual(
|
||||||
|
Delta.create(
|
||||||
|
{ x: 100, y: 100, version: 2, versionNonce: 2 },
|
||||||
|
{ x: 200, y: 200, version: 3, versionNonce: 3 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should squash different delta types ", () => {
|
||||||
|
// id1: added -> updated => added
|
||||||
|
// id2: removed -> added => added
|
||||||
|
// id3: updated -> removed => removed
|
||||||
|
const elementsDelta1 = ElementsDelta.create(
|
||||||
|
{
|
||||||
|
id1: Delta.create(
|
||||||
|
{ x: 100, version: 1, versionNonce: 1, isDeleted: true },
|
||||||
|
{ x: 101, version: 2, versionNonce: 2, isDeleted: false },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id2: Delta.create(
|
||||||
|
{ x: 200, version: 1, versionNonce: 1, isDeleted: false },
|
||||||
|
{ x: 201, version: 2, versionNonce: 2, isDeleted: true },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id3: Delta.create(
|
||||||
|
{ x: 300, version: 1, versionNonce: 1 },
|
||||||
|
{ x: 301, version: 2, versionNonce: 2 },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsDelta2 = ElementsDelta.create(
|
||||||
|
{
|
||||||
|
id2: Delta.create(
|
||||||
|
{ y: 200, version: 2, versionNonce: 2, isDeleted: true },
|
||||||
|
{ y: 201, version: 3, versionNonce: 3, isDeleted: false },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id3: Delta.create(
|
||||||
|
{ y: 300, version: 2, versionNonce: 2, isDeleted: false },
|
||||||
|
{ y: 301, version: 3, versionNonce: 3, isDeleted: true },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id1: Delta.create(
|
||||||
|
{ y: 100, version: 2, versionNonce: 2 },
|
||||||
|
{ y: 101, version: 3, versionNonce: 3 },
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
||||||
|
|
||||||
|
expect(elementsDelta.isEmpty()).toBeFalsy();
|
||||||
|
expect(elementsDelta).toBe(elementsDelta1);
|
||||||
|
expect(elementsDelta.added).toEqual({
|
||||||
|
id1: Delta.create(
|
||||||
|
{ x: 100, y: 100, version: 2, versionNonce: 2, isDeleted: true },
|
||||||
|
{ x: 101, y: 101, version: 3, versionNonce: 3, isDeleted: false },
|
||||||
|
),
|
||||||
|
id2: Delta.create(
|
||||||
|
{ x: 200, y: 200, version: 2, versionNonce: 2, isDeleted: true },
|
||||||
|
{ x: 201, y: 201, version: 3, versionNonce: 3, isDeleted: false },
|
||||||
|
),
|
||||||
|
});
|
||||||
|
expect(elementsDelta.removed).toEqual({
|
||||||
|
id3: Delta.create(
|
||||||
|
{ x: 300, y: 300, version: 2, versionNonce: 2, isDeleted: false },
|
||||||
|
{ x: 301, y: 301, version: 3, versionNonce: 3, isDeleted: true },
|
||||||
|
),
|
||||||
|
});
|
||||||
|
expect(elementsDelta.updated).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should squash bound elements", () => {
|
||||||
|
const elementsDelta1 = ElementsDelta.create(
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
id1: Delta.create(
|
||||||
|
{
|
||||||
|
version: 1,
|
||||||
|
versionNonce: 1,
|
||||||
|
boundElements: [{ id: "t1", type: "text" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
versionNonce: 2,
|
||||||
|
boundElements: [{ id: "t2", type: "text" }],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsDelta2 = ElementsDelta.create(
|
||||||
|
{},
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
id1: Delta.create(
|
||||||
|
{
|
||||||
|
version: 2,
|
||||||
|
versionNonce: 2,
|
||||||
|
boundElements: [{ id: "a1", type: "arrow" }],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: 3,
|
||||||
|
versionNonce: 3,
|
||||||
|
boundElements: [{ id: "a2", type: "arrow" }],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const elementsDelta = elementsDelta1.squash(elementsDelta2);
|
||||||
|
|
||||||
|
expect(elementsDelta.updated.id1.deleted.boundElements).toEqual([
|
||||||
|
{ id: "t1", type: "text" },
|
||||||
|
{ id: "a1", type: "arrow" },
|
||||||
|
]);
|
||||||
|
expect(elementsDelta.updated.id1.inserted.boundElements).toEqual([
|
||||||
|
{ id: "t2", type: "text" },
|
||||||
|
{ id: "a2", type: "arrow" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("AppStateDelta", () => {
|
describe("AppStateDelta", () => {
|
||||||
describe("ensure stable delta properties order", () => {
|
describe("ensure stable delta properties order", () => {
|
||||||
it("should maintain stable order for root properties", () => {
|
it("should maintain stable order for root properties", () => {
|
||||||
const name = "untitled scene";
|
const name = "untitled scene";
|
||||||
const selectedLinearElementId = "id1" as LinearElementEditor["elementId"];
|
const selectedLinearElement = {
|
||||||
|
elementId: "id1" as LinearElementEditor["elementId"],
|
||||||
|
isEditing: false,
|
||||||
|
};
|
||||||
|
|
||||||
const commonAppState = {
|
const commonAppState = {
|
||||||
viewBackgroundColor: "#ffffff",
|
viewBackgroundColor: "#ffffff",
|
||||||
@@ -24,23 +339,23 @@ describe("AppStateDelta", () => {
|
|||||||
const prevAppState1: ObservedAppState = {
|
const prevAppState1: ObservedAppState = {
|
||||||
...commonAppState,
|
...commonAppState,
|
||||||
name: "",
|
name: "",
|
||||||
selectedLinearElementId: null,
|
selectedLinearElement: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextAppState1: ObservedAppState = {
|
const nextAppState1: ObservedAppState = {
|
||||||
...commonAppState,
|
...commonAppState,
|
||||||
name,
|
name,
|
||||||
selectedLinearElementId,
|
selectedLinearElement,
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevAppState2: ObservedAppState = {
|
const prevAppState2: ObservedAppState = {
|
||||||
selectedLinearElementId: null,
|
selectedLinearElement: null,
|
||||||
name: "",
|
name: "",
|
||||||
...commonAppState,
|
...commonAppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextAppState2: ObservedAppState = {
|
const nextAppState2: ObservedAppState = {
|
||||||
selectedLinearElementId,
|
selectedLinearElement,
|
||||||
name,
|
name,
|
||||||
...commonAppState,
|
...commonAppState,
|
||||||
};
|
};
|
||||||
@@ -58,9 +373,7 @@ describe("AppStateDelta", () => {
|
|||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
croppingElementId: null,
|
croppingElementId: null,
|
||||||
selectedLinearElementId: null,
|
selectedLinearElement: null,
|
||||||
selectedLinearElementIsEditing: null,
|
|
||||||
editingLinearElementId: null,
|
|
||||||
activeLockedId: null,
|
activeLockedId: null,
|
||||||
lockedMultiSelections: {},
|
lockedMultiSelections: {},
|
||||||
};
|
};
|
||||||
@@ -106,9 +419,7 @@ describe("AppStateDelta", () => {
|
|||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
croppingElementId: null,
|
croppingElementId: null,
|
||||||
selectedLinearElementId: null,
|
selectedLinearElement: null,
|
||||||
selectedLinearElementIsEditing: null,
|
|
||||||
editingLinearElementId: null,
|
|
||||||
activeLockedId: null,
|
activeLockedId: null,
|
||||||
lockedMultiSelections: {},
|
lockedMultiSelections: {},
|
||||||
};
|
};
|
||||||
@@ -149,4 +460,97 @@ describe("AppStateDelta", () => {
|
|||||||
expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
|
expect(JSON.stringify(delta1)).toBe(JSON.stringify(delta2));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("squash", () => {
|
||||||
|
it("should not squash when second delta is empty", () => {
|
||||||
|
const delta = Delta.create(
|
||||||
|
{ name: "untitled scene" },
|
||||||
|
{ name: "titled scene" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const appStateDelta1 = AppStateDelta.create(delta);
|
||||||
|
const appStateDelta2 = AppStateDelta.empty();
|
||||||
|
const appStateDelta = appStateDelta1.squash(appStateDelta2);
|
||||||
|
|
||||||
|
expect(appStateDelta.isEmpty()).toBeFalsy();
|
||||||
|
expect(appStateDelta).toBe(appStateDelta1);
|
||||||
|
expect(appStateDelta.delta).toBe(delta);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should squash exclusive properties", () => {
|
||||||
|
const delta1 = Delta.create(
|
||||||
|
{ name: "untitled scene" },
|
||||||
|
{ name: "titled scene" },
|
||||||
|
);
|
||||||
|
const delta2 = Delta.create(
|
||||||
|
{ viewBackgroundColor: "#ffffff" },
|
||||||
|
{ viewBackgroundColor: "#000000" },
|
||||||
|
);
|
||||||
|
|
||||||
|
const appStateDelta1 = AppStateDelta.create(delta1);
|
||||||
|
const appStateDelta2 = AppStateDelta.create(delta2);
|
||||||
|
const appStateDelta = appStateDelta1.squash(appStateDelta2);
|
||||||
|
|
||||||
|
expect(appStateDelta.isEmpty()).toBeFalsy();
|
||||||
|
expect(appStateDelta).toBe(appStateDelta1);
|
||||||
|
expect(appStateDelta.delta).toEqual(
|
||||||
|
Delta.create(
|
||||||
|
{ name: "untitled scene", viewBackgroundColor: "#ffffff" },
|
||||||
|
{ name: "titled scene", viewBackgroundColor: "#000000" },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should squash selectedElementIds, selectedGroupIds and lockedMultiSelections", () => {
|
||||||
|
const delta1 = Delta.create<Partial<ObservedAppState>>(
|
||||||
|
{
|
||||||
|
name: "untitled scene",
|
||||||
|
selectedElementIds: { id1: true },
|
||||||
|
selectedGroupIds: {},
|
||||||
|
lockedMultiSelections: { g1: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "titled scene",
|
||||||
|
selectedElementIds: { id2: true },
|
||||||
|
selectedGroupIds: { g1: true },
|
||||||
|
lockedMultiSelections: {},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const delta2 = Delta.create<Partial<ObservedAppState>>(
|
||||||
|
{
|
||||||
|
selectedElementIds: { id3: true },
|
||||||
|
selectedGroupIds: { g1: true },
|
||||||
|
lockedMultiSelections: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selectedElementIds: { id2: true },
|
||||||
|
selectedGroupIds: { g2: true, g3: true },
|
||||||
|
lockedMultiSelections: { g3: true },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const appStateDelta1 = AppStateDelta.create(delta1);
|
||||||
|
const appStateDelta2 = AppStateDelta.create(delta2);
|
||||||
|
const appStateDelta = appStateDelta1.squash(appStateDelta2);
|
||||||
|
|
||||||
|
expect(appStateDelta.isEmpty()).toBeFalsy();
|
||||||
|
expect(appStateDelta).toBe(appStateDelta1);
|
||||||
|
expect(appStateDelta.delta).toEqual(
|
||||||
|
Delta.create<Partial<ObservedAppState>>(
|
||||||
|
{
|
||||||
|
name: "untitled scene",
|
||||||
|
selectedElementIds: { id1: true, id3: true },
|
||||||
|
selectedGroupIds: { g1: true },
|
||||||
|
lockedMultiSelections: { g1: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "titled scene",
|
||||||
|
selectedElementIds: { id2: true },
|
||||||
|
selectedGroupIds: { g1: true, g2: true, g3: true },
|
||||||
|
lockedMultiSelections: { g3: true },
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const actionClearCanvas = register({
|
|||||||
pasteDialog: appState.pasteDialog,
|
pasteDialog: appState.pasteDialog,
|
||||||
activeTool:
|
activeTool:
|
||||||
appState.activeTool.type === "image"
|
appState.activeTool.type === "image"
|
||||||
? { ...appState.activeTool, type: "selection" }
|
? { ...appState.activeTool, type: app.defaultSelectionTool }
|
||||||
: appState.activeTool,
|
: appState.activeTool,
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
captureUpdate: CaptureUpdateAction.IMMEDIATELY,
|
||||||
@@ -494,13 +494,13 @@ export const actionToggleEraserTool = register({
|
|||||||
name: "toggleEraserTool",
|
name: "toggleEraserTool",
|
||||||
label: "toolBar.eraser",
|
label: "toolBar.eraser",
|
||||||
trackEvent: { category: "toolbar" },
|
trackEvent: { category: "toolbar" },
|
||||||
perform: (elements, appState) => {
|
perform: (elements, appState, _, app) => {
|
||||||
let activeTool: AppState["activeTool"];
|
let activeTool: AppState["activeTool"];
|
||||||
|
|
||||||
if (isEraserActive(appState)) {
|
if (isEraserActive(appState)) {
|
||||||
activeTool = updateActiveTool(appState, {
|
activeTool = updateActiveTool(appState, {
|
||||||
...(appState.activeTool.lastActiveTool || {
|
...(appState.activeTool.lastActiveTool || {
|
||||||
type: "selection",
|
type: app.defaultSelectionTool,
|
||||||
}),
|
}),
|
||||||
lastActiveToolBeforeEraser: null,
|
lastActiveToolBeforeEraser: null,
|
||||||
});
|
});
|
||||||
@@ -530,6 +530,9 @@ export const actionToggleLassoTool = register({
|
|||||||
label: "toolBar.lasso",
|
label: "toolBar.lasso",
|
||||||
icon: LassoIcon,
|
icon: LassoIcon,
|
||||||
trackEvent: { category: "toolbar" },
|
trackEvent: { category: "toolbar" },
|
||||||
|
predicate: (elements, appState, props, app) => {
|
||||||
|
return app.defaultSelectionTool !== "lasso";
|
||||||
|
},
|
||||||
perform: (elements, appState, _, app) => {
|
perform: (elements, appState, _, app) => {
|
||||||
let activeTool: AppState["activeTool"];
|
let activeTool: AppState["activeTool"];
|
||||||
|
|
||||||
|
|||||||
@@ -298,7 +298,9 @@ export const actionDeleteSelected = register({
|
|||||||
elements: nextElements,
|
elements: nextElements,
|
||||||
appState: {
|
appState: {
|
||||||
...nextAppState,
|
...nextAppState,
|
||||||
activeTool: updateActiveTool(appState, { type: "selection" }),
|
activeTool: updateActiveTool(appState, {
|
||||||
|
type: app.defaultSelectionTool,
|
||||||
|
}),
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
activeEmbeddable: null,
|
activeEmbeddable: null,
|
||||||
selectedLinearElement: null,
|
selectedLinearElement: null,
|
||||||
|
|||||||
@@ -5,7 +5,11 @@ import {
|
|||||||
bindOrUnbindLinearElement,
|
bindOrUnbindLinearElement,
|
||||||
isBindingEnabled,
|
isBindingEnabled,
|
||||||
} from "@excalidraw/element/binding";
|
} from "@excalidraw/element/binding";
|
||||||
import { isValidPolygon, LinearElementEditor } from "@excalidraw/element";
|
import {
|
||||||
|
isValidPolygon,
|
||||||
|
LinearElementEditor,
|
||||||
|
newElementWith,
|
||||||
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isBindingElement,
|
isBindingElement,
|
||||||
@@ -78,7 +82,14 @@ export const actionFinalize = register({
|
|||||||
let newElements = elements;
|
let newElements = elements;
|
||||||
if (element && isInvisiblySmallElement(element)) {
|
if (element && isInvisiblySmallElement(element)) {
|
||||||
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
||||||
newElements = newElements.filter((el) => el.id !== element!.id);
|
newElements = newElements.map((el) => {
|
||||||
|
if (el.id === element.id) {
|
||||||
|
return newElementWith(el, {
|
||||||
|
isDeleted: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
elements: newElements,
|
elements: newElements,
|
||||||
@@ -117,7 +128,12 @@ export const actionFinalize = register({
|
|||||||
return {
|
return {
|
||||||
elements:
|
elements:
|
||||||
element.points.length < 2 || isInvisiblySmallElement(element)
|
element.points.length < 2 || isInvisiblySmallElement(element)
|
||||||
? elements.filter((el) => el.id !== element.id)
|
? elements.map((el) => {
|
||||||
|
if (el.id === element.id) {
|
||||||
|
return newElementWith(el, { isDeleted: true });
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
})
|
||||||
: undefined,
|
: undefined,
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
@@ -172,7 +188,12 @@ export const actionFinalize = register({
|
|||||||
|
|
||||||
if (element && isInvisiblySmallElement(element)) {
|
if (element && isInvisiblySmallElement(element)) {
|
||||||
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
// TODO: #7348 in theory this gets recorded by the store, so the invisible elements could be restored by the undo/redo, which might be not what we would want
|
||||||
newElements = newElements.filter((el) => el.id !== element!.id);
|
newElements = newElements.map((el) => {
|
||||||
|
if (el.id === element?.id) {
|
||||||
|
return newElementWith(el, { isDeleted: true });
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
if (isLinearElement(element) || isFreeDrawElement(element)) {
|
||||||
@@ -240,13 +261,13 @@ export const actionFinalize = register({
|
|||||||
if (appState.activeTool.type === "eraser") {
|
if (appState.activeTool.type === "eraser") {
|
||||||
activeTool = updateActiveTool(appState, {
|
activeTool = updateActiveTool(appState, {
|
||||||
...(appState.activeTool.lastActiveTool || {
|
...(appState.activeTool.lastActiveTool || {
|
||||||
type: "selection",
|
type: app.defaultSelectionTool,
|
||||||
}),
|
}),
|
||||||
lastActiveToolBeforeEraser: null,
|
lastActiveToolBeforeEraser: null,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
activeTool = updateActiveTool(appState, {
|
activeTool = updateActiveTool(appState, {
|
||||||
type: "selection",
|
type: app.defaultSelectionTool,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import {
|
|||||||
hasStrokeWidth,
|
hasStrokeWidth,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
|
|
||||||
import { SHAPES } from "./shapes";
|
import { getToolbarTools } from "./shapes";
|
||||||
|
|
||||||
import "./Actions.scss";
|
import "./Actions.scss";
|
||||||
|
|
||||||
@@ -295,7 +295,8 @@ export const ShapesSwitcher = ({
|
|||||||
|
|
||||||
const frameToolSelected = activeTool.type === "frame";
|
const frameToolSelected = activeTool.type === "frame";
|
||||||
const laserToolSelected = activeTool.type === "laser";
|
const laserToolSelected = activeTool.type === "laser";
|
||||||
const lassoToolSelected = activeTool.type === "lasso";
|
const lassoToolSelected =
|
||||||
|
activeTool.type === "lasso" && app.defaultSelectionTool !== "lasso";
|
||||||
|
|
||||||
const embeddableToolSelected = activeTool.type === "embeddable";
|
const embeddableToolSelected = activeTool.type === "embeddable";
|
||||||
|
|
||||||
@@ -303,63 +304,68 @@ export const ShapesSwitcher = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{SHAPES.map(({ value, icon, key, numericKey, fillable }, index) => {
|
{getToolbarTools(app).map(
|
||||||
if (
|
({ value, icon, key, numericKey, fillable }, index) => {
|
||||||
UIOptions.tools?.[
|
if (
|
||||||
value as Extract<typeof value, keyof AppProps["UIOptions"]["tools"]>
|
UIOptions.tools?.[
|
||||||
] === false
|
value as Extract<
|
||||||
) {
|
typeof value,
|
||||||
return null;
|
keyof AppProps["UIOptions"]["tools"]
|
||||||
}
|
>
|
||||||
|
] === false
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const label = t(`toolBar.${value}`);
|
const label = t(`toolBar.${value}`);
|
||||||
const letter =
|
const letter =
|
||||||
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
key && capitalizeString(typeof key === "string" ? key : key[0]);
|
||||||
const shortcut = letter
|
const shortcut = letter
|
||||||
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
? `${letter} ${t("helpDialog.or")} ${numericKey}`
|
||||||
: `${numericKey}`;
|
: `${numericKey}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolButton
|
<ToolButton
|
||||||
className={clsx("Shape", { fillable })}
|
className={clsx("Shape", { fillable })}
|
||||||
key={value}
|
key={value}
|
||||||
type="radio"
|
type="radio"
|
||||||
icon={icon}
|
icon={icon}
|
||||||
checked={activeTool.type === value}
|
checked={activeTool.type === value}
|
||||||
name="editor-current-shape"
|
name="editor-current-shape"
|
||||||
title={`${capitalizeString(label)} — ${shortcut}`}
|
title={`${capitalizeString(label)} — ${shortcut}`}
|
||||||
keyBindingLabel={numericKey || letter}
|
keyBindingLabel={numericKey || letter}
|
||||||
aria-label={capitalizeString(label)}
|
aria-label={capitalizeString(label)}
|
||||||
aria-keyshortcuts={shortcut}
|
aria-keyshortcuts={shortcut}
|
||||||
data-testid={`toolbar-${value}`}
|
data-testid={`toolbar-${value}`}
|
||||||
onPointerDown={({ pointerType }) => {
|
onPointerDown={({ pointerType }) => {
|
||||||
if (!appState.penDetected && pointerType === "pen") {
|
if (!appState.penDetected && pointerType === "pen") {
|
||||||
app.togglePenMode(true);
|
app.togglePenMode(true);
|
||||||
}
|
|
||||||
|
|
||||||
if (value === "selection") {
|
|
||||||
if (appState.activeTool.type === "selection") {
|
|
||||||
app.setActiveTool({ type: "lasso" });
|
|
||||||
} else {
|
|
||||||
app.setActiveTool({ type: "selection" });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}}
|
if (value === "selection") {
|
||||||
onChange={({ pointerType }) => {
|
if (appState.activeTool.type === "selection") {
|
||||||
if (appState.activeTool.type !== value) {
|
app.setActiveTool({ type: "lasso" });
|
||||||
trackEvent("toolbar", value, "ui");
|
} else {
|
||||||
}
|
app.setActiveTool({ type: "selection" });
|
||||||
if (value === "image") {
|
}
|
||||||
app.setActiveTool({
|
}
|
||||||
type: value,
|
}}
|
||||||
});
|
onChange={({ pointerType }) => {
|
||||||
} else {
|
if (appState.activeTool.type !== value) {
|
||||||
app.setActiveTool({ type: value });
|
trackEvent("toolbar", value, "ui");
|
||||||
}
|
}
|
||||||
}}
|
if (value === "image") {
|
||||||
/>
|
app.setActiveTool({
|
||||||
);
|
type: value,
|
||||||
})}
|
});
|
||||||
|
} else {
|
||||||
|
app.setActiveTool({ type: value });
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
<div className="App-toolbar__divider" />
|
<div className="App-toolbar__divider" />
|
||||||
|
|
||||||
<DropdownMenu open={isExtraToolsMenuOpen}>
|
<DropdownMenu open={isExtraToolsMenuOpen}>
|
||||||
@@ -418,14 +424,16 @@ export const ShapesSwitcher = ({
|
|||||||
>
|
>
|
||||||
{t("toolBar.laser")}
|
{t("toolBar.laser")}
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item
|
{app.defaultSelectionTool !== "lasso" && (
|
||||||
onSelect={() => app.setActiveTool({ type: "lasso" })}
|
<DropdownMenu.Item
|
||||||
icon={LassoIcon}
|
onSelect={() => app.setActiveTool({ type: "lasso" })}
|
||||||
data-testid="toolbar-lasso"
|
icon={LassoIcon}
|
||||||
selected={lassoToolSelected}
|
data-testid="toolbar-lasso"
|
||||||
>
|
selected={lassoToolSelected}
|
||||||
{t("toolBar.lasso")}
|
>
|
||||||
</DropdownMenu.Item>
|
{t("toolBar.lasso")}
|
||||||
|
</DropdownMenu.Item>
|
||||||
|
)}
|
||||||
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
|
<div style={{ margin: "6px 0", fontSize: 14, fontWeight: 600 }}>
|
||||||
Generate
|
Generate
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ import {
|
|||||||
randomInteger,
|
randomInteger,
|
||||||
CLASSES,
|
CLASSES,
|
||||||
Emitter,
|
Emitter,
|
||||||
|
isMobile,
|
||||||
MINIMUM_ARROW_SIZE,
|
MINIMUM_ARROW_SIZE,
|
||||||
} from "@excalidraw/common";
|
} from "@excalidraw/common";
|
||||||
|
|
||||||
@@ -233,6 +234,8 @@ import {
|
|||||||
hitElementBoundingBox,
|
hitElementBoundingBox,
|
||||||
isLineElement,
|
isLineElement,
|
||||||
isSimpleArrow,
|
isSimpleArrow,
|
||||||
|
StoreDelta,
|
||||||
|
type ApplyToOptions,
|
||||||
} from "@excalidraw/element";
|
} from "@excalidraw/element";
|
||||||
|
|
||||||
import type { LocalPoint, Radians } from "@excalidraw/math";
|
import type { LocalPoint, Radians } from "@excalidraw/math";
|
||||||
@@ -259,6 +262,7 @@ import type {
|
|||||||
MagicGenerationData,
|
MagicGenerationData,
|
||||||
ExcalidrawArrowElement,
|
ExcalidrawArrowElement,
|
||||||
ExcalidrawElbowArrowElement,
|
ExcalidrawElbowArrowElement,
|
||||||
|
SceneElementsMap,
|
||||||
} from "@excalidraw/element/types";
|
} from "@excalidraw/element/types";
|
||||||
|
|
||||||
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
import type { Mutable, ValueOf } from "@excalidraw/common/utility-types";
|
||||||
@@ -650,9 +654,14 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
>();
|
>();
|
||||||
onRemoveEventListenersEmitter = new Emitter<[]>();
|
onRemoveEventListenersEmitter = new Emitter<[]>();
|
||||||
|
|
||||||
|
defaultSelectionTool: "selection" | "lasso" = "selection";
|
||||||
|
|
||||||
constructor(props: AppProps) {
|
constructor(props: AppProps) {
|
||||||
super(props);
|
super(props);
|
||||||
const defaultAppState = getDefaultAppState();
|
const defaultAppState = getDefaultAppState();
|
||||||
|
this.defaultSelectionTool = this.isMobileOrTablet()
|
||||||
|
? ("lasso" as const)
|
||||||
|
: ("selection" as const);
|
||||||
const {
|
const {
|
||||||
excalidrawAPI,
|
excalidrawAPI,
|
||||||
viewModeEnabled = false,
|
viewModeEnabled = false,
|
||||||
@@ -697,6 +706,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (excalidrawAPI) {
|
if (excalidrawAPI) {
|
||||||
const api: ExcalidrawImperativeAPI = {
|
const api: ExcalidrawImperativeAPI = {
|
||||||
updateScene: this.updateScene,
|
updateScene: this.updateScene,
|
||||||
|
applyDeltas: this.applyDeltas,
|
||||||
mutateElement: this.mutateElement,
|
mutateElement: this.mutateElement,
|
||||||
updateLibrary: this.library.updateLibrary,
|
updateLibrary: this.library.updateLibrary,
|
||||||
addFiles: this.addFiles,
|
addFiles: this.addFiles,
|
||||||
@@ -1602,7 +1612,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
renderWelcomeScreen={
|
renderWelcomeScreen={
|
||||||
!this.state.isLoading &&
|
!this.state.isLoading &&
|
||||||
this.state.showWelcomeScreen &&
|
this.state.showWelcomeScreen &&
|
||||||
this.state.activeTool.type === "selection" &&
|
this.state.activeTool.type ===
|
||||||
|
this.defaultSelectionTool &&
|
||||||
!this.state.zenModeEnabled &&
|
!this.state.zenModeEnabled &&
|
||||||
!this.scene.getElementsIncludingDeleted().length
|
!this.scene.getElementsIncludingDeleted().length
|
||||||
}
|
}
|
||||||
@@ -2342,7 +2353,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const scene = restore(initialData, null, null, { repairBindings: true });
|
const scene = restore(initialData, null, null, {
|
||||||
|
repairBindings: true,
|
||||||
|
deleteInvisibleElements: true,
|
||||||
|
});
|
||||||
|
const activeTool = scene.appState.activeTool;
|
||||||
scene.appState = {
|
scene.appState = {
|
||||||
...scene.appState,
|
...scene.appState,
|
||||||
theme: this.props.theme || scene.appState.theme,
|
theme: this.props.theme || scene.appState.theme,
|
||||||
@@ -2352,8 +2367,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// with a library install link, which should auto-open the library)
|
// with a library install link, which should auto-open the library)
|
||||||
openSidebar: scene.appState?.openSidebar || this.state.openSidebar,
|
openSidebar: scene.appState?.openSidebar || this.state.openSidebar,
|
||||||
activeTool:
|
activeTool:
|
||||||
scene.appState.activeTool.type === "image"
|
activeTool.type === "image" ||
|
||||||
? { ...scene.appState.activeTool, type: "selection" }
|
activeTool.type === "lasso" ||
|
||||||
|
activeTool.type === "selection"
|
||||||
|
? {
|
||||||
|
...activeTool,
|
||||||
|
type: this.defaultSelectionTool,
|
||||||
|
}
|
||||||
: scene.appState.activeTool,
|
: scene.appState.activeTool,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
toast: this.state.toast,
|
toast: this.state.toast,
|
||||||
@@ -2392,6 +2412,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private isMobileOrTablet = (): boolean => {
|
||||||
|
const hasTouch = "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
||||||
|
const hasCoarsePointer =
|
||||||
|
"matchMedia" in window &&
|
||||||
|
window?.matchMedia("(pointer: coarse)")?.matches;
|
||||||
|
const isTouchMobile = hasTouch && hasCoarsePointer;
|
||||||
|
|
||||||
|
return isMobile || isTouchMobile;
|
||||||
|
};
|
||||||
|
|
||||||
private isMobileBreakpoint = (width: number, height: number) => {
|
private isMobileBreakpoint = (width: number, height: number) => {
|
||||||
return (
|
return (
|
||||||
width < MQ_MAX_WIDTH_PORTRAIT ||
|
width < MQ_MAX_WIDTH_PORTRAIT ||
|
||||||
@@ -3110,7 +3140,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements,
|
elements,
|
||||||
files: data.files || null,
|
files: data.files || null,
|
||||||
position: "cursor",
|
position: this.isMobileOrTablet() ? "center" : "cursor",
|
||||||
retainSeed: isPlainPaste,
|
retainSeed: isPlainPaste,
|
||||||
});
|
});
|
||||||
} else if (data.text) {
|
} else if (data.text) {
|
||||||
@@ -3128,7 +3158,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.addElementsFromPasteOrLibrary({
|
this.addElementsFromPasteOrLibrary({
|
||||||
elements,
|
elements,
|
||||||
files,
|
files,
|
||||||
position: "cursor",
|
position: this.isMobileOrTablet() ? "center" : "cursor",
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -3188,7 +3218,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
this.addTextFromPaste(data.text, isPlainPaste);
|
this.addTextFromPaste(data.text, isPlainPaste);
|
||||||
}
|
}
|
||||||
this.setActiveTool({ type: "selection" });
|
this.setActiveTool({ type: this.defaultSelectionTool }, true);
|
||||||
event?.preventDefault();
|
event?.preventDefault();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -3200,7 +3230,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
retainSeed?: boolean;
|
retainSeed?: boolean;
|
||||||
fitToContent?: boolean;
|
fitToContent?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const elements = restoreElements(opts.elements, null, undefined);
|
const elements = restoreElements(opts.elements, null, {
|
||||||
|
deleteInvisibleElements: true,
|
||||||
|
});
|
||||||
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
const [minX, minY, maxX, maxY] = getCommonBounds(elements);
|
||||||
|
|
||||||
const elementsCenterX = distance(minX, maxX) / 2;
|
const elementsCenterX = distance(minX, maxX) / 2;
|
||||||
@@ -3332,7 +3364,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
this.setActiveTool({ type: "selection" });
|
this.setActiveTool({ type: this.defaultSelectionTool }, true);
|
||||||
|
|
||||||
if (opts.fitToContent) {
|
if (opts.fitToContent) {
|
||||||
this.scrollToContent(duplicatedElements, {
|
this.scrollToContent(duplicatedElements, {
|
||||||
@@ -3578,7 +3610,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
...updateActiveTool(
|
...updateActiveTool(
|
||||||
this.state,
|
this.state,
|
||||||
prevState.activeTool.locked
|
prevState.activeTool.locked
|
||||||
? { type: "selection" }
|
? { type: this.defaultSelectionTool }
|
||||||
: prevState.activeTool,
|
: prevState.activeTool,
|
||||||
),
|
),
|
||||||
locked: !prevState.activeTool.locked,
|
locked: !prevState.activeTool.locked,
|
||||||
@@ -3933,6 +3965,27 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public applyDeltas = (
|
||||||
|
deltas: StoreDelta[],
|
||||||
|
options?: ApplyToOptions,
|
||||||
|
): [SceneElementsMap, AppState, boolean] => {
|
||||||
|
// squash all deltas together, starting with a fresh new delta instance
|
||||||
|
const aggregatedDelta = StoreDelta.squash(...deltas);
|
||||||
|
|
||||||
|
// create new instance of elements map & appState, so we don't accidentaly mutate existing ones
|
||||||
|
const nextAppState = { ...this.state };
|
||||||
|
const nextElements = new Map(
|
||||||
|
this.scene.getElementsMapIncludingDeleted(),
|
||||||
|
) as SceneElementsMap;
|
||||||
|
|
||||||
|
return StoreDelta.applyTo(
|
||||||
|
aggregatedDelta,
|
||||||
|
nextElements,
|
||||||
|
nextAppState,
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
public mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
public mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||||
element: TElement,
|
element: TElement,
|
||||||
updates: ElementUpdate<TElement>,
|
updates: ElementUpdate<TElement>,
|
||||||
@@ -4470,7 +4523,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!this.state.selectionElement &&
|
!this.state.selectionElement &&
|
||||||
!this.state.selectedElementsAreBeingDragged
|
!this.state.selectedElementsAreBeingDragged
|
||||||
) {
|
) {
|
||||||
const shape = findShapeByKey(event.key);
|
const shape = findShapeByKey(event.key, this);
|
||||||
if (shape) {
|
if (shape) {
|
||||||
if (this.state.activeTool.type !== shape) {
|
if (this.state.activeTool.type !== shape) {
|
||||||
trackEvent(
|
trackEvent(
|
||||||
@@ -4563,7 +4616,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
|
if (event.key === KEYS.K && !event.altKey && !event[KEYS.CTRL_OR_CMD]) {
|
||||||
if (this.state.activeTool.type === "laser") {
|
if (this.state.activeTool.type === "laser") {
|
||||||
this.setActiveTool({ type: "selection" });
|
this.setActiveTool({ type: this.defaultSelectionTool });
|
||||||
} else {
|
} else {
|
||||||
this.setActiveTool({ type: "laser" });
|
this.setActiveTool({ type: "laser" });
|
||||||
}
|
}
|
||||||
@@ -4927,17 +4980,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}),
|
}),
|
||||||
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
onSubmit: withBatchedUpdates(({ viaKeyboard, nextOriginalText }) => {
|
||||||
const isDeleted = !nextOriginalText.trim();
|
const isDeleted = !nextOriginalText.trim();
|
||||||
|
updateElement(nextOriginalText, isDeleted);
|
||||||
|
|
||||||
if (isDeleted && !isExistingElement) {
|
|
||||||
// let's just remove the element from the scene, as it's an empty just created text element
|
|
||||||
this.scene.replaceAllElements(
|
|
||||||
this.scene
|
|
||||||
.getElementsIncludingDeleted()
|
|
||||||
.filter((x) => x.id !== element.id),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
updateElement(nextOriginalText, isDeleted);
|
|
||||||
}
|
|
||||||
// select the created text element only if submitting via keyboard
|
// select the created text element only if submitting via keyboard
|
||||||
// (when submitting via click it should act as signal to deselect)
|
// (when submitting via click it should act as signal to deselect)
|
||||||
if (!isDeleted && viaKeyboard) {
|
if (!isDeleted && viaKeyboard) {
|
||||||
@@ -4961,15 +5005,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
fixBindingsAfterDeletion(this.scene.getNonDeletedElements(), [
|
fixBindingsAfterDeletion(this.scene.getNonDeletedElements(), [
|
||||||
element,
|
element,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// we need to record either way, whether the text element was added or removed
|
if (!isDeleted || isExistingElement) {
|
||||||
// since we need to sync this delta to other clients, otherwise it would end up with inconsistencies
|
this.store.scheduleCapture();
|
||||||
this.store.scheduleCapture();
|
}
|
||||||
|
|
||||||
flushSync(() => {
|
flushSync(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -5416,7 +5461,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// we should only be able to double click when mode is selection
|
// we should only be able to double click when mode is selection
|
||||||
if (this.state.activeTool.type !== "selection") {
|
if (this.state.activeTool.type !== this.defaultSelectionTool) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5708,8 +5753,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
const frames = this.scene
|
const frames = this.scene
|
||||||
.getNonDeletedFramesLikes()
|
.getNonDeletedFramesLikes()
|
||||||
.filter((frame): frame is ExcalidrawFrameLikeElement =>
|
.filter(
|
||||||
isCursorInFrame(sceneCoords, frame, elementsMap),
|
(frame): frame is ExcalidrawFrameLikeElement =>
|
||||||
|
!frame.locked && isCursorInFrame(sceneCoords, frame, elementsMap),
|
||||||
);
|
);
|
||||||
|
|
||||||
return frames.length ? frames[frames.length - 1] : null;
|
return frames.length ? frames[frames.length - 1] : null;
|
||||||
@@ -6027,6 +6073,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (
|
if (
|
||||||
hasDeselectedButton ||
|
hasDeselectedButton ||
|
||||||
(this.state.activeTool.type !== "selection" &&
|
(this.state.activeTool.type !== "selection" &&
|
||||||
|
this.state.activeTool.type !== "lasso" &&
|
||||||
this.state.activeTool.type !== "text" &&
|
this.state.activeTool.type !== "text" &&
|
||||||
this.state.activeTool.type !== "eraser")
|
this.state.activeTool.type !== "eraser")
|
||||||
) {
|
) {
|
||||||
@@ -6189,7 +6236,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!isElbowArrow(hitElement) ||
|
!isElbowArrow(hitElement) ||
|
||||||
!(hitElement.startBinding || hitElement.endBinding)
|
!(hitElement.startBinding || hitElement.endBinding)
|
||||||
) {
|
) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
if (
|
||||||
|
this.state.activeTool.type !== "lasso" ||
|
||||||
|
selectedElements.length > 0
|
||||||
|
) {
|
||||||
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
|
}
|
||||||
if (this.state.activeEmbeddable?.state === "hover") {
|
if (this.state.activeEmbeddable?.state === "hover") {
|
||||||
this.setState({ activeEmbeddable: null });
|
this.setState({ activeEmbeddable: null });
|
||||||
}
|
}
|
||||||
@@ -6306,7 +6358,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!isElbowArrow(element) ||
|
!isElbowArrow(element) ||
|
||||||
!(element.startBinding || element.endBinding)
|
!(element.startBinding || element.endBinding)
|
||||||
) {
|
) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
if (
|
||||||
|
this.state.activeTool.type !== "lasso" ||
|
||||||
|
Object.keys(this.state.selectedElementIds).length > 0
|
||||||
|
) {
|
||||||
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
} else if (this.hitElement(scenePointerX, scenePointerY, element)) {
|
||||||
@@ -6315,7 +6372,12 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
!isElbowArrow(element) ||
|
!isElbowArrow(element) ||
|
||||||
!(element.startBinding || element.endBinding)
|
!(element.startBinding || element.endBinding)
|
||||||
) {
|
) {
|
||||||
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
if (
|
||||||
|
this.state.activeTool.type !== "lasso" ||
|
||||||
|
Object.keys(this.state.selectedElementIds).length > 0
|
||||||
|
) {
|
||||||
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6577,11 +6639,119 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.activeTool.type === "lasso") {
|
if (this.state.activeTool.type === "lasso") {
|
||||||
this.lassoTrail.startPath(
|
const hitSelectedElement =
|
||||||
pointerDownState.origin.x,
|
pointerDownState.hit.element &&
|
||||||
pointerDownState.origin.y,
|
this.isASelectedElement(pointerDownState.hit.element);
|
||||||
event.shiftKey,
|
|
||||||
);
|
const isMobileOrTablet = this.isMobileOrTablet();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements &&
|
||||||
|
!pointerDownState.resize.handleType &&
|
||||||
|
!hitSelectedElement
|
||||||
|
) {
|
||||||
|
this.lassoTrail.startPath(
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
event.shiftKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
// block dragging after lasso selection on PCs until the next pointer down
|
||||||
|
// (on mobile or tablet, we want to allow user to drag immediately)
|
||||||
|
pointerDownState.drag.blockDragging = !isMobileOrTablet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only for mobile or tablet, if we hit an element, select it immediately like normal selection
|
||||||
|
if (
|
||||||
|
isMobileOrTablet &&
|
||||||
|
pointerDownState.hit.element &&
|
||||||
|
!hitSelectedElement
|
||||||
|
) {
|
||||||
|
this.setState((prevState) => {
|
||||||
|
const nextSelectedElementIds: { [id: string]: true } = {
|
||||||
|
...prevState.selectedElementIds,
|
||||||
|
[pointerDownState.hit.element!.id]: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const previouslySelectedElements: ExcalidrawElement[] = [];
|
||||||
|
|
||||||
|
Object.keys(prevState.selectedElementIds).forEach((id) => {
|
||||||
|
const element = this.scene.getElement(id);
|
||||||
|
element && previouslySelectedElements.push(element);
|
||||||
|
});
|
||||||
|
|
||||||
|
const hitElement = pointerDownState.hit.element!;
|
||||||
|
|
||||||
|
// if hitElement is frame-like, deselect all of its elements
|
||||||
|
// if they are selected
|
||||||
|
if (isFrameLikeElement(hitElement)) {
|
||||||
|
getFrameChildren(previouslySelectedElements, hitElement.id).forEach(
|
||||||
|
(element) => {
|
||||||
|
delete nextSelectedElementIds[element.id];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else if (hitElement.frameId) {
|
||||||
|
// if hitElement is in a frame and its frame has been selected
|
||||||
|
// disable selection for the given element
|
||||||
|
if (nextSelectedElementIds[hitElement.frameId]) {
|
||||||
|
delete nextSelectedElementIds[hitElement.id];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// hitElement is neither a frame nor an element in a frame
|
||||||
|
// but since hitElement could be in a group with some frames
|
||||||
|
// this means selecting hitElement will have the frames selected as well
|
||||||
|
// because we want to keep the invariant:
|
||||||
|
// - frames and their elements are not selected at the same time
|
||||||
|
// we deselect elements in those frames that were previously selected
|
||||||
|
|
||||||
|
const groupIds = hitElement.groupIds;
|
||||||
|
const framesInGroups = new Set(
|
||||||
|
groupIds
|
||||||
|
.flatMap((gid) =>
|
||||||
|
getElementsInGroup(this.scene.getNonDeletedElements(), gid),
|
||||||
|
)
|
||||||
|
.filter((element) => isFrameLikeElement(element))
|
||||||
|
.map((frame) => frame.id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (framesInGroups.size > 0) {
|
||||||
|
previouslySelectedElements.forEach((element) => {
|
||||||
|
if (element.frameId && framesInGroups.has(element.frameId)) {
|
||||||
|
// deselect element and groups containing the element
|
||||||
|
delete nextSelectedElementIds[element.id];
|
||||||
|
element.groupIds
|
||||||
|
.flatMap((gid) =>
|
||||||
|
getElementsInGroup(
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
gid,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.forEach((element) => {
|
||||||
|
delete nextSelectedElementIds[element.id];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...selectGroupsForSelectedElements(
|
||||||
|
{
|
||||||
|
editingGroupId: prevState.editingGroupId,
|
||||||
|
selectedElementIds: nextSelectedElementIds,
|
||||||
|
},
|
||||||
|
this.scene.getNonDeletedElements(),
|
||||||
|
prevState,
|
||||||
|
this,
|
||||||
|
),
|
||||||
|
showHyperlinkPopup:
|
||||||
|
hitElement.link || isEmbeddableElement(hitElement)
|
||||||
|
? "info"
|
||||||
|
: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
pointerDownState.hit.wasAddedToSelection = true;
|
||||||
|
}
|
||||||
} else if (this.state.activeTool.type === "text") {
|
} else if (this.state.activeTool.type === "text") {
|
||||||
this.handleTextOnPointerDown(event, pointerDownState);
|
this.handleTextOnPointerDown(event, pointerDownState);
|
||||||
} else if (
|
} else if (
|
||||||
@@ -6961,6 +7131,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
hasOccurred: false,
|
hasOccurred: false,
|
||||||
offset: null,
|
offset: null,
|
||||||
origin: { ...origin },
|
origin: { ...origin },
|
||||||
|
blockDragging: false,
|
||||||
},
|
},
|
||||||
eventListeners: {
|
eventListeners: {
|
||||||
onMove: null,
|
onMove: null,
|
||||||
@@ -7036,7 +7207,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event: React.PointerEvent<HTMLElement>,
|
event: React.PointerEvent<HTMLElement>,
|
||||||
pointerDownState: PointerDownState,
|
pointerDownState: PointerDownState,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
if (this.state.activeTool.type === "selection") {
|
if (
|
||||||
|
this.state.activeTool.type === "selection" ||
|
||||||
|
this.state.activeTool.type === "lasso"
|
||||||
|
) {
|
||||||
const elements = this.scene.getNonDeletedElements();
|
const elements = this.scene.getNonDeletedElements();
|
||||||
const elementsMap = this.scene.getNonDeletedElementsMap();
|
const elementsMap = this.scene.getNonDeletedElementsMap();
|
||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
@@ -7243,7 +7417,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// on CMD/CTRL, drill down to hit element regardless of groups etc.
|
// on CMD/CTRL, drill down to hit element regardless of groups etc.
|
||||||
if (event[KEYS.CTRL_OR_CMD]) {
|
if (event[KEYS.CTRL_OR_CMD]) {
|
||||||
if (event.altKey) {
|
if (event.altKey) {
|
||||||
// ctrl + alt means we're lasso selecting
|
// ctrl + alt means we're lasso selecting - start lasso trail and switch to lasso tool
|
||||||
|
|
||||||
|
// Close any open dialogs that might interfere with lasso selection
|
||||||
|
if (this.state.openDialog?.name === "elementLinkSelector") {
|
||||||
|
this.setOpenDialog(null);
|
||||||
|
}
|
||||||
|
this.lassoTrail.startPath(
|
||||||
|
pointerDownState.origin.x,
|
||||||
|
pointerDownState.origin.y,
|
||||||
|
event.shiftKey,
|
||||||
|
);
|
||||||
|
this.setActiveTool({ type: "lasso", fromSelection: true });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!this.state.selectedElementIds[hitElement.id]) {
|
if (!this.state.selectedElementIds[hitElement.id]) {
|
||||||
@@ -7464,7 +7649,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
if (!this.state.activeTool.locked) {
|
if (!this.state.activeTool.locked) {
|
||||||
this.setState({
|
this.setState({
|
||||||
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
activeTool: updateActiveTool(this.state, {
|
||||||
|
type: this.defaultSelectionTool,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -8248,15 +8435,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
this.state.selectedLinearElement.elementId ===
|
this.state.selectedLinearElement.elementId ===
|
||||||
pointerDownState.hit.element?.id;
|
pointerDownState.hit.element?.id;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(hasHitASelectedElement ||
|
(hasHitASelectedElement ||
|
||||||
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
|
||||||
!isSelectingPointsInLineEditor &&
|
!isSelectingPointsInLineEditor &&
|
||||||
this.state.activeTool.type !== "lasso"
|
!pointerDownState.drag.blockDragging
|
||||||
) {
|
) {
|
||||||
const selectedElements = this.scene.getSelectedElements(this.state);
|
const selectedElements = this.scene.getSelectedElements(this.state);
|
||||||
|
if (
|
||||||
if (selectedElements.every((element) => element.locked)) {
|
selectedElements.length > 0 &&
|
||||||
|
selectedElements.every((element) => element.locked)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8277,6 +8467,29 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// if elements should be deselected on pointerup
|
// if elements should be deselected on pointerup
|
||||||
pointerDownState.drag.hasOccurred = true;
|
pointerDownState.drag.hasOccurred = true;
|
||||||
|
|
||||||
|
// prevent immediate dragging during lasso selection to avoid element displacement
|
||||||
|
// only allow dragging if we're not in the middle of lasso selection
|
||||||
|
// (on mobile, allow dragging if we hit an element)
|
||||||
|
if (
|
||||||
|
this.state.activeTool.type === "lasso" &&
|
||||||
|
this.lassoTrail.hasCurrentTrail &&
|
||||||
|
!(this.isMobileOrTablet() && pointerDownState.hit.element) &&
|
||||||
|
!this.state.activeTool.fromSelection
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear lasso trail when starting to drag selected elements with lasso tool
|
||||||
|
// Only clear if we're actually dragging (not during lasso selection)
|
||||||
|
if (
|
||||||
|
this.state.activeTool.type === "lasso" &&
|
||||||
|
selectedElements.length > 0 &&
|
||||||
|
pointerDownState.drag.hasOccurred &&
|
||||||
|
!this.state.activeTool.fromSelection
|
||||||
|
) {
|
||||||
|
this.lassoTrail.endPath();
|
||||||
|
}
|
||||||
|
|
||||||
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
// prevent dragging even if we're no longer holding cmd/ctrl otherwise
|
||||||
// it would have weird results (stuff jumping all over the screen)
|
// it would have weird results (stuff jumping all over the screen)
|
||||||
// Checking for editingTextElement to avoid jump while editing on mobile #6503
|
// Checking for editingTextElement to avoid jump while editing on mobile #6503
|
||||||
@@ -8871,6 +9084,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
): (event: PointerEvent) => void {
|
): (event: PointerEvent) => void {
|
||||||
return withBatchedUpdates((childEvent: PointerEvent) => {
|
return withBatchedUpdates((childEvent: PointerEvent) => {
|
||||||
this.removePointer(childEvent);
|
this.removePointer(childEvent);
|
||||||
|
pointerDownState.drag.blockDragging = false;
|
||||||
if (pointerDownState.eventListeners.onMove) {
|
if (pointerDownState.eventListeners.onMove) {
|
||||||
pointerDownState.eventListeners.onMove.flush();
|
pointerDownState.eventListeners.onMove.flush();
|
||||||
}
|
}
|
||||||
@@ -9159,7 +9373,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
newElement: null,
|
newElement: null,
|
||||||
activeTool: updateActiveTool(this.state, {
|
activeTool: updateActiveTool(this.state, {
|
||||||
type: "selection",
|
type: this.defaultSelectionTool,
|
||||||
}),
|
}),
|
||||||
selectedElementIds: makeNextSelectedElementIds(
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
{
|
{
|
||||||
@@ -9775,7 +9989,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
newElement: null,
|
newElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
activeTool: updateActiveTool(this.state, {
|
||||||
|
type: this.defaultSelectionTool,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -10069,7 +10285,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState(
|
this.setState(
|
||||||
{
|
{
|
||||||
newElement: null,
|
newElement: null,
|
||||||
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
activeTool: updateActiveTool(this.state, {
|
||||||
|
type: this.defaultSelectionTool,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.actionManager.executeAction(actionFinalize);
|
this.actionManager.executeAction(actionFinalize);
|
||||||
@@ -10121,7 +10339,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (erroredFiles.size) {
|
if (erroredFiles.size) {
|
||||||
this.store.scheduleAction(CaptureUpdateAction.NEVER);
|
this.store.scheduleAction(CaptureUpdateAction.NEVER);
|
||||||
this.scene.replaceAllElements(
|
this.scene.replaceAllElements(
|
||||||
elements.map((element) => {
|
this.scene.getElementsIncludingDeleted().map((element) => {
|
||||||
if (
|
if (
|
||||||
isInitializedImageElement(element) &&
|
isInitializedImageElement(element) &&
|
||||||
erroredFiles.has(element.fileId)
|
erroredFiles.has(element.fileId)
|
||||||
@@ -10442,7 +10660,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
event.nativeEvent.pointerType === "pen" &&
|
event.nativeEvent.pointerType === "pen" &&
|
||||||
// always allow if user uses a pen secondary button
|
// always allow if user uses a pen secondary button
|
||||||
event.button !== POINTER_BUTTON.SECONDARY)) &&
|
event.button !== POINTER_BUTTON.SECONDARY)) &&
|
||||||
this.state.activeTool.type !== "selection"
|
this.state.activeTool.type !== this.defaultSelectionTool
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import {
|
|||||||
EraserIcon,
|
EraserIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
|
|
||||||
|
import type { AppClassProperties } from "../types";
|
||||||
|
|
||||||
export const SHAPES = [
|
export const SHAPES = [
|
||||||
{
|
{
|
||||||
icon: SelectionIcon,
|
icon: SelectionIcon,
|
||||||
@@ -86,8 +88,23 @@ export const SHAPES = [
|
|||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const findShapeByKey = (key: string) => {
|
export const getToolbarTools = (app: AppClassProperties) => {
|
||||||
const shape = SHAPES.find((shape, index) => {
|
return app.defaultSelectionTool === "lasso"
|
||||||
|
? ([
|
||||||
|
{
|
||||||
|
value: "lasso",
|
||||||
|
icon: SelectionIcon,
|
||||||
|
key: KEYS.V,
|
||||||
|
numericKey: KEYS["1"],
|
||||||
|
fillable: true,
|
||||||
|
},
|
||||||
|
...SHAPES.slice(1),
|
||||||
|
] as const)
|
||||||
|
: SHAPES;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findShapeByKey = (key: string, app: AppClassProperties) => {
|
||||||
|
const shape = getToolbarTools(app).find((shape, index) => {
|
||||||
return (
|
return (
|
||||||
(shape.numericKey != null && key === shape.numericKey.toString()) ||
|
(shape.numericKey != null && key === shape.numericKey.toString()) ||
|
||||||
(shape.key &&
|
(shape.key &&
|
||||||
|
|||||||
@@ -170,7 +170,11 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
},
|
},
|
||||||
localAppState,
|
localAppState,
|
||||||
localElements,
|
localElements,
|
||||||
{ repairBindings: true, refreshDimensions: false },
|
{
|
||||||
|
repairBindings: true,
|
||||||
|
refreshDimensions: false,
|
||||||
|
deleteInvisibleElements: true,
|
||||||
|
},
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
} else if (isValidLibrary(data)) {
|
} else if (isValidLibrary(data)) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export type ReconciledExcalidrawElement = OrderedExcalidrawElement &
|
|||||||
export type RemoteExcalidrawElement = OrderedExcalidrawElement &
|
export type RemoteExcalidrawElement = OrderedExcalidrawElement &
|
||||||
MakeBrand<"RemoteExcalidrawElement">;
|
MakeBrand<"RemoteExcalidrawElement">;
|
||||||
|
|
||||||
const shouldDiscardRemoteElement = (
|
export const shouldDiscardRemoteElement = (
|
||||||
localAppState: AppState,
|
localAppState: AppState,
|
||||||
local: OrderedExcalidrawElement | undefined,
|
local: OrderedExcalidrawElement | undefined,
|
||||||
remote: RemoteExcalidrawElement,
|
remote: RemoteExcalidrawElement,
|
||||||
@@ -30,7 +30,7 @@ const shouldDiscardRemoteElement = (
|
|||||||
// local element is being edited
|
// local element is being edited
|
||||||
(local.id === localAppState.editingTextElement?.id ||
|
(local.id === localAppState.editingTextElement?.id ||
|
||||||
local.id === localAppState.resizingElement?.id ||
|
local.id === localAppState.resizingElement?.id ||
|
||||||
local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array
|
local.id === localAppState.newElement?.id ||
|
||||||
// local element is newer
|
// local element is newer
|
||||||
local.version > remote.version ||
|
local.version > remote.version ||
|
||||||
// resolve conflicting edits deterministically by taking the one with
|
// resolve conflicting edits deterministically by taking the one with
|
||||||
|
|||||||
@@ -241,8 +241,9 @@ const restoreElementWithProperties = <
|
|||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const restoreElement = (
|
export const restoreElement = (
|
||||||
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
element: Exclude<ExcalidrawElement, ExcalidrawSelectionElement>,
|
||||||
|
opts?: { deleteInvisibleElements?: boolean },
|
||||||
): typeof element | null => {
|
): typeof element | null => {
|
||||||
element = { ...element };
|
element = { ...element };
|
||||||
|
|
||||||
@@ -290,7 +291,8 @@ const restoreElement = (
|
|||||||
|
|
||||||
// if empty text, mark as deleted. We keep in array
|
// if empty text, mark as deleted. We keep in array
|
||||||
// for data integrity purposes (collab etc.)
|
// for data integrity purposes (collab etc.)
|
||||||
if (!text && !element.isDeleted) {
|
if (opts?.deleteInvisibleElements && !text && !element.isDeleted) {
|
||||||
|
// TODO: we should not do this since it breaks sync / versioning when we exchange / apply just deltas and restore the elements (deletion isn't recorded)
|
||||||
element = { ...element, originalText: text, isDeleted: true };
|
element = { ...element, originalText: text, isDeleted: true };
|
||||||
element = bumpVersion(element);
|
element = bumpVersion(element);
|
||||||
}
|
}
|
||||||
@@ -523,7 +525,13 @@ export const restoreElements = (
|
|||||||
elements: ImportedDataState["elements"],
|
elements: ImportedDataState["elements"],
|
||||||
/** NOTE doesn't serve for reconciliation */
|
/** NOTE doesn't serve for reconciliation */
|
||||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
opts?: { refreshDimensions?: boolean; repairBindings?: boolean } | undefined,
|
opts?:
|
||||||
|
| {
|
||||||
|
refreshDimensions?: boolean;
|
||||||
|
repairBindings?: boolean;
|
||||||
|
deleteInvisibleElements?: boolean;
|
||||||
|
}
|
||||||
|
| undefined,
|
||||||
): OrderedExcalidrawElement[] => {
|
): OrderedExcalidrawElement[] => {
|
||||||
// used to detect duplicate top-level element ids
|
// used to detect duplicate top-level element ids
|
||||||
const existingIds = new Set<string>();
|
const existingIds = new Set<string>();
|
||||||
@@ -532,24 +540,38 @@ export const restoreElements = (
|
|||||||
(elements || []).reduce((elements, element) => {
|
(elements || []).reduce((elements, element) => {
|
||||||
// filtering out selection, which is legacy, no longer kept in elements,
|
// filtering out selection, which is legacy, no longer kept in elements,
|
||||||
// and causing issues if retained
|
// and causing issues if retained
|
||||||
if (element.type !== "selection" && !isInvisiblySmallElement(element)) {
|
if (element.type === "selection") {
|
||||||
let migratedElement: ExcalidrawElement | null = restoreElement(element);
|
return elements;
|
||||||
if (migratedElement) {
|
|
||||||
const localElement = localElementsMap?.get(element.id);
|
|
||||||
if (localElement && localElement.version > migratedElement.version) {
|
|
||||||
migratedElement = bumpVersion(
|
|
||||||
migratedElement,
|
|
||||||
localElement.version,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (existingIds.has(migratedElement.id)) {
|
|
||||||
migratedElement = { ...migratedElement, id: randomId() };
|
|
||||||
}
|
|
||||||
existingIds.add(migratedElement.id);
|
|
||||||
|
|
||||||
elements.push(migratedElement);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let migratedElement: ExcalidrawElement | null = restoreElement(element, {
|
||||||
|
deleteInvisibleElements: opts?.deleteInvisibleElements,
|
||||||
|
});
|
||||||
|
if (migratedElement) {
|
||||||
|
const localElement = localElementsMap?.get(element.id);
|
||||||
|
|
||||||
|
const shouldMarkAsDeleted =
|
||||||
|
opts?.deleteInvisibleElements && isInvisiblySmallElement(element);
|
||||||
|
|
||||||
|
if (
|
||||||
|
shouldMarkAsDeleted ||
|
||||||
|
(localElement && localElement.version > migratedElement.version)
|
||||||
|
) {
|
||||||
|
migratedElement = bumpVersion(migratedElement, localElement?.version);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldMarkAsDeleted) {
|
||||||
|
migratedElement = { ...migratedElement, isDeleted: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingIds.has(migratedElement.id)) {
|
||||||
|
migratedElement = { ...migratedElement, id: randomId() };
|
||||||
|
}
|
||||||
|
existingIds.add(migratedElement.id);
|
||||||
|
|
||||||
|
elements.push(migratedElement);
|
||||||
|
}
|
||||||
|
|
||||||
return elements;
|
return elements;
|
||||||
}, [] as ExcalidrawElement[]),
|
}, [] as ExcalidrawElement[]),
|
||||||
);
|
);
|
||||||
@@ -790,7 +812,11 @@ export const restore = (
|
|||||||
*/
|
*/
|
||||||
localAppState: Partial<AppState> | null | undefined,
|
localAppState: Partial<AppState> | null | undefined,
|
||||||
localElements: readonly ExcalidrawElement[] | null | undefined,
|
localElements: readonly ExcalidrawElement[] | null | undefined,
|
||||||
elementsConfig?: { refreshDimensions?: boolean; repairBindings?: boolean },
|
elementsConfig?: {
|
||||||
|
refreshDimensions?: boolean;
|
||||||
|
repairBindings?: boolean;
|
||||||
|
deleteInvisibleElements?: boolean;
|
||||||
|
},
|
||||||
): RestoredDataState => {
|
): RestoredDataState => {
|
||||||
return {
|
return {
|
||||||
elements: restoreElements(data?.elements, localElements, elementsConfig),
|
elements: restoreElements(data?.elements, localElements, elementsConfig),
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ export class History {
|
|||||||
let nextAppState = appState;
|
let nextAppState = appState;
|
||||||
let containsVisibleChange = false;
|
let containsVisibleChange = false;
|
||||||
|
|
||||||
// iterate through the history entries in case ;they result in no visible changes
|
// iterate through the history entries in case they result in no visible changes
|
||||||
while (historyDelta) {
|
while (historyDelta) {
|
||||||
try {
|
try {
|
||||||
[nextElements, nextAppState, containsVisibleChange] =
|
[nextElements, nextAppState, containsVisibleChange] =
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ export { defaultLang, useI18n, languages } from "./i18n";
|
|||||||
export {
|
export {
|
||||||
restore,
|
restore,
|
||||||
restoreAppState,
|
restoreAppState,
|
||||||
|
restoreElement,
|
||||||
restoreElements,
|
restoreElements,
|
||||||
restoreLibraryItems,
|
restoreLibraryItems,
|
||||||
} from "./data/restore";
|
} from "./data/restore";
|
||||||
|
|||||||
@@ -169,8 +169,14 @@ export const isSnappingEnabled = ({
|
|||||||
selectedElements: NonDeletedExcalidrawElement[];
|
selectedElements: NonDeletedExcalidrawElement[];
|
||||||
}) => {
|
}) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
|
// Allow snapping for lasso tool when dragging selected elements
|
||||||
|
// but not during lasso selection phase
|
||||||
|
const isLassoDragging =
|
||||||
|
app.state.activeTool.type === "lasso" &&
|
||||||
|
app.state.selectedElementsAreBeingDragged;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
app.state.activeTool.type !== "lasso" &&
|
(app.state.activeTool.type !== "lasso" || isLassoDragging) &&
|
||||||
((app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) ||
|
((app.state.objectsSnapModeEnabled && !event[KEYS.CTRL_OR_CMD]) ||
|
||||||
(!app.state.objectsSnapModeEnabled &&
|
(!app.state.objectsSnapModeEnabled &&
|
||||||
event[KEYS.CTRL_OR_CMD] &&
|
event[KEYS.CTRL_OR_CMD] &&
|
||||||
|
|||||||
@@ -3682,14 +3682,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1116226695,
|
"seed": 400692809,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 23633383,
|
"versionNonce": 81784553,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": 20,
|
"x": 20,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@@ -3714,14 +3714,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"opacity": 100,
|
"opacity": 100,
|
||||||
"roughness": 1,
|
"roughness": 1,
|
||||||
"roundness": null,
|
"roundness": null,
|
||||||
"seed": 1278240551,
|
"seed": 449462985,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 2,
|
"strokeWidth": 2,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 401146281,
|
"versionNonce": 1150084233,
|
||||||
"width": 20,
|
"width": 20,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2216,16 +2216,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] undo
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"updated": {
|
"updated": {},
|
||||||
"id0": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 5,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 3,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"id": "id6",
|
"id": "id6",
|
||||||
},
|
},
|
||||||
@@ -6394,15 +6385,16 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id9": true,
|
"id9": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": "id9",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id9",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6470,13 +6462,19 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id12": true,
|
"id12": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": "id12",
|
"selectedLinearElement": {
|
||||||
|
"elementId": "id12",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id9": true,
|
"id9": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": "id9",
|
"selectedLinearElement": {
|
||||||
|
"elementId": "id9",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6542,15 +6540,16 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id15": true,
|
"id15": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id12": true,
|
"id12": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": "id12",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id12",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6677,12 +6676,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"selectedLinearElementId": "id15",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id15",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6700,15 +6700,16 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id22": true,
|
"id22": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id15": true,
|
"id15": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": "id15",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id15",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6833,12 +6834,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"selectedLinearElementId": "id22",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id22",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -6873,12 +6875,13 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedLinearElementId": "id22",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id22",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -8719,13 +8722,14 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": "id0",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id0",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -8946,13 +8950,14 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": "id0",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id0",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -9365,13 +9370,14 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": "id0",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id0",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -9770,13 +9776,14 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1
|
|||||||
"selectedElementIds": {
|
"selectedElementIds": {
|
||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"selectedLinearElementId": "id0",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id0",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedElementIds": {},
|
"selectedElementIds": {},
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -10885,32 +10892,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"updated": {
|
"updated": {},
|
||||||
"id0": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 6,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id3": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 6,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"id6": {
|
|
||||||
"deleted": {
|
|
||||||
"version": 6,
|
|
||||||
},
|
|
||||||
"inserted": {
|
|
||||||
"version": 4,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"id": "id21",
|
"id": "id21",
|
||||||
},
|
},
|
||||||
@@ -14494,12 +14476,13 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st
|
|||||||
"appState": AppStateDelta {
|
"appState": AppStateDelta {
|
||||||
"delta": Delta {
|
"delta": Delta {
|
||||||
"deleted": {
|
"deleted": {
|
||||||
"selectedLinearElementId": null,
|
"selectedLinearElement": null,
|
||||||
"selectedLinearElementIsEditing": null,
|
|
||||||
},
|
},
|
||||||
"inserted": {
|
"inserted": {
|
||||||
"selectedLinearElementId": "id6",
|
"selectedLinearElement": {
|
||||||
"selectedLinearElementIsEditing": false,
|
"elementId": "id6",
|
||||||
|
"isEditing": false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ describe("restoreElements", () => {
|
|||||||
const rectElement = API.createElement({ type: "rectangle" });
|
const rectElement = API.createElement({ type: "rectangle" });
|
||||||
mockSizeHelper.mockImplementation(() => true);
|
mockSizeHelper.mockImplementation(() => true);
|
||||||
|
|
||||||
expect(restore.restoreElements([rectElement], null).length).toBe(0);
|
expect(
|
||||||
|
restore.restoreElements([rectElement], null, {
|
||||||
|
deleteInvisibleElements: true,
|
||||||
|
}),
|
||||||
|
).toEqual([expect.objectContaining({ isDeleted: true })]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should restore text element correctly passing value for each attribute", () => {
|
it("should restore text element correctly passing value for each attribute", () => {
|
||||||
@@ -85,6 +89,23 @@ describe("restoreElements", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not delete empty text element when opts.deleteInvisibleElements is not defined", () => {
|
||||||
|
const textElement = API.createElement({
|
||||||
|
type: "text",
|
||||||
|
text: "",
|
||||||
|
isDeleted: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const restoredElements = restore.restoreElements([textElement], null);
|
||||||
|
|
||||||
|
expect(restoredElements).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: textElement.id,
|
||||||
|
isDeleted: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it("should restore text element correctly with unknown font family, null text and undefined alignment", () => {
|
it("should restore text element correctly with unknown font family, null text and undefined alignment", () => {
|
||||||
const textElement: any = API.createElement({
|
const textElement: any = API.createElement({
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -97,10 +118,9 @@ describe("restoreElements", () => {
|
|||||||
textElement.font = "10 unknown";
|
textElement.font = "10 unknown";
|
||||||
|
|
||||||
expect(textElement.isDeleted).toBe(false);
|
expect(textElement.isDeleted).toBe(false);
|
||||||
const restoredText = restore.restoreElements(
|
const restoredText = restore.restoreElements([textElement], null, {
|
||||||
[textElement],
|
deleteInvisibleElements: true,
|
||||||
null,
|
})[0] as ExcalidrawTextElement;
|
||||||
)[0] as ExcalidrawTextElement;
|
|
||||||
expect(restoredText.isDeleted).toBe(true);
|
expect(restoredText.isDeleted).toBe(true);
|
||||||
expect(restoredText).toMatchSnapshot({
|
expect(restoredText).toMatchSnapshot({
|
||||||
seed: expect.any(Number),
|
seed: expect.any(Number),
|
||||||
@@ -177,13 +197,16 @@ describe("restoreElements", () => {
|
|||||||
y: 0,
|
y: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const restoredElements = restore.restoreElements([arrowElement], null);
|
const restoredElements = restore.restoreElements([arrowElement], null, {
|
||||||
|
deleteInvisibleElements: true,
|
||||||
|
});
|
||||||
|
|
||||||
const restoredArrow = restoredElements[0] as
|
const restoredArrow = restoredElements[0] as
|
||||||
| ExcalidrawArrowElement
|
| ExcalidrawArrowElement
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
expect(restoredArrow).toBeUndefined();
|
expect(restoredArrow).not.toBeUndefined();
|
||||||
|
expect(restoredArrow?.isDeleted).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should keep 'imperceptibly' small freedraw/line elements", () => {
|
it("should keep 'imperceptibly' small freedraw/line elements", () => {
|
||||||
@@ -848,6 +871,7 @@ describe("repairing bindings", () => {
|
|||||||
let restoredElements = restore.restoreElements(
|
let restoredElements = restore.restoreElements(
|
||||||
[container, invisibleBoundElement, boundElement],
|
[container, invisibleBoundElement, boundElement],
|
||||||
null,
|
null,
|
||||||
|
{ deleteInvisibleElements: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(restoredElements).toEqual([
|
expect(restoredElements).toEqual([
|
||||||
@@ -855,6 +879,11 @@ describe("repairing bindings", () => {
|
|||||||
id: container.id,
|
id: container.id,
|
||||||
boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding],
|
boundElements: [obsoleteBinding, invisibleBinding, nonExistentBinding],
|
||||||
}),
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: invisibleBoundElement.id,
|
||||||
|
containerId: container.id,
|
||||||
|
isDeleted: true,
|
||||||
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: boundElement.id,
|
id: boundElement.id,
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
@@ -864,7 +893,7 @@ describe("repairing bindings", () => {
|
|||||||
restoredElements = restore.restoreElements(
|
restoredElements = restore.restoreElements(
|
||||||
[container, invisibleBoundElement, boundElement],
|
[container, invisibleBoundElement, boundElement],
|
||||||
null,
|
null,
|
||||||
{ repairBindings: true },
|
{ repairBindings: true, deleteInvisibleElements: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(restoredElements).toEqual([
|
expect(restoredElements).toEqual([
|
||||||
@@ -872,6 +901,11 @@ describe("repairing bindings", () => {
|
|||||||
id: container.id,
|
id: container.id,
|
||||||
boundElements: [],
|
boundElements: [],
|
||||||
}),
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: invisibleBoundElement.id,
|
||||||
|
containerId: container.id,
|
||||||
|
isDeleted: true,
|
||||||
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: boundElement.id,
|
id: boundElement.id,
|
||||||
containerId: container.id,
|
containerId: container.id,
|
||||||
|
|||||||
@@ -315,7 +315,12 @@ describe("Test dragCreate", () => {
|
|||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "arrow",
|
||||||
|
isDeleted: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("line", async () => {
|
it("line", async () => {
|
||||||
@@ -344,7 +349,12 @@ describe("Test dragCreate", () => {
|
|||||||
);
|
);
|
||||||
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`5`);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
type: "line",
|
||||||
|
isDeleted: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3043,15 +3043,14 @@ describe("history", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Keyboard.undo();
|
Keyboard.undo();
|
||||||
expect(API.getUndoStack().length).toBe(3);
|
expect(API.getUndoStack().length).toBe(0);
|
||||||
expect(API.getRedoStack().length).toBe(1);
|
expect(API.getRedoStack().length).toBe(4);
|
||||||
expect(h.state.selectedLinearElement).not.toBeNull();
|
expect(h.state.selectedLinearElement).toBeNull();
|
||||||
expect(h.state.selectedLinearElement?.isEditing).toBe(true);
|
|
||||||
|
|
||||||
Keyboard.redo();
|
Keyboard.redo();
|
||||||
expect(API.getUndoStack().length).toBe(4);
|
expect(API.getUndoStack().length).toBe(4);
|
||||||
expect(API.getRedoStack().length).toBe(0);
|
expect(API.getRedoStack().length).toBe(0);
|
||||||
expect(h.state.selectedLinearElement).not.toBeNull();
|
expect(h.state.selectedLinearElement).toBeNull();
|
||||||
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
expect(h.state.selectedLinearElement?.isEditing ?? false).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4056,7 +4055,7 @@ describe("history", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: container.id,
|
id: container.id,
|
||||||
boundElements: [{ id: remoteText.id, type: "text" }],
|
boundElements: [{ id: remoteText.id, type: "text" }],
|
||||||
isDeleted: false, // isDeleted got remotely updated to false
|
isDeleted: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: text.id,
|
id: text.id,
|
||||||
@@ -4066,7 +4065,7 @@ describe("history", () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: remoteText.id,
|
id: remoteText.id,
|
||||||
// unbound
|
// unbound
|
||||||
containerId: container.id,
|
containerId: null,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -248,8 +248,10 @@ export type ObservedElementsAppState = {
|
|||||||
editingGroupId: AppState["editingGroupId"];
|
editingGroupId: AppState["editingGroupId"];
|
||||||
selectedElementIds: AppState["selectedElementIds"];
|
selectedElementIds: AppState["selectedElementIds"];
|
||||||
selectedGroupIds: AppState["selectedGroupIds"];
|
selectedGroupIds: AppState["selectedGroupIds"];
|
||||||
selectedLinearElementId: LinearElementEditor["elementId"] | null;
|
selectedLinearElement: {
|
||||||
selectedLinearElementIsEditing: boolean | null;
|
elementId: LinearElementEditor["elementId"];
|
||||||
|
isEditing: boolean;
|
||||||
|
} | null;
|
||||||
croppingElementId: AppState["croppingElementId"];
|
croppingElementId: AppState["croppingElementId"];
|
||||||
lockedMultiSelections: AppState["lockedMultiSelections"];
|
lockedMultiSelections: AppState["lockedMultiSelections"];
|
||||||
activeLockedId: AppState["activeLockedId"];
|
activeLockedId: AppState["activeLockedId"];
|
||||||
@@ -729,6 +731,8 @@ export type AppClassProperties = {
|
|||||||
|
|
||||||
onPointerUpEmitter: App["onPointerUpEmitter"];
|
onPointerUpEmitter: App["onPointerUpEmitter"];
|
||||||
updateEditorAtom: App["updateEditorAtom"];
|
updateEditorAtom: App["updateEditorAtom"];
|
||||||
|
|
||||||
|
defaultSelectionTool: "selection" | "lasso";
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PointerDownState = Readonly<{
|
export type PointerDownState = Readonly<{
|
||||||
@@ -778,6 +782,10 @@ export type PointerDownState = Readonly<{
|
|||||||
// by default same as PointerDownState.origin. On alt-duplication, reset
|
// by default same as PointerDownState.origin. On alt-duplication, reset
|
||||||
// to current pointer position at time of duplication.
|
// to current pointer position at time of duplication.
|
||||||
origin: { x: number; y: number };
|
origin: { x: number; y: number };
|
||||||
|
// Whether to block drag after lasso selection
|
||||||
|
// this is meant to be used to block dragging after lasso selection on PCs
|
||||||
|
// until the next pointer down
|
||||||
|
blockDragging: boolean;
|
||||||
};
|
};
|
||||||
// We need to have these in the state so that we can unsubscribe them
|
// We need to have these in the state so that we can unsubscribe them
|
||||||
eventListeners: {
|
eventListeners: {
|
||||||
@@ -799,6 +807,7 @@ export type UnsubscribeCallback = () => void;
|
|||||||
|
|
||||||
export interface ExcalidrawImperativeAPI {
|
export interface ExcalidrawImperativeAPI {
|
||||||
updateScene: InstanceType<typeof App>["updateScene"];
|
updateScene: InstanceType<typeof App>["updateScene"];
|
||||||
|
applyDeltas: InstanceType<typeof App>["applyDeltas"];
|
||||||
mutateElement: InstanceType<typeof App>["mutateElement"];
|
mutateElement: InstanceType<typeof App>["mutateElement"];
|
||||||
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
|
updateLibrary: InstanceType<typeof Library>["updateLibrary"];
|
||||||
resetScene: InstanceType<typeof App>["resetScene"];
|
resetScene: InstanceType<typeof App>["resetScene"];
|
||||||
|
|||||||
@@ -704,7 +704,7 @@ describe("textWysiwyg", () => {
|
|||||||
rectangle.x + rectangle.width / 2,
|
rectangle.x + rectangle.width / 2,
|
||||||
rectangle.y + rectangle.height / 2,
|
rectangle.y + rectangle.height / 2,
|
||||||
);
|
);
|
||||||
expect(h.elements.length).toBe(2);
|
expect(h.elements.length).toBe(3);
|
||||||
|
|
||||||
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
text = h.elements[1] as ExcalidrawTextElementWithContainer;
|
||||||
expect(text.type).toBe("text");
|
expect(text.type).toBe("text");
|
||||||
@@ -1198,7 +1198,11 @@ describe("textWysiwyg", () => {
|
|||||||
updateTextEditor(editor, " ");
|
updateTextEditor(editor, " ");
|
||||||
Keyboard.exitTextEditor(editor);
|
Keyboard.exitTextEditor(editor);
|
||||||
expect(rectangle.boundElements).toStrictEqual([]);
|
expect(rectangle.boundElements).toStrictEqual([]);
|
||||||
expect(h.elements[1]).toBeUndefined();
|
expect(h.elements[1]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
isDeleted: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should restore original container height and clear cache once text is unbind", async () => {
|
it("should restore original container height and clear cache once text is unbind", async () => {
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export const exportToCanvas = ({
|
|||||||
{ elements, appState },
|
{ elements, appState },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
{ deleteInvisibleElements: true },
|
||||||
);
|
);
|
||||||
const { exportBackground, viewBackgroundColor } = restoredAppState;
|
const { exportBackground, viewBackgroundColor } = restoredAppState;
|
||||||
return _exportToCanvas(
|
return _exportToCanvas(
|
||||||
@@ -179,6 +180,7 @@ export const exportToSvg = async ({
|
|||||||
{ elements, appState },
|
{ elements, appState },
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
|
{ deleteInvisibleElements: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
const exportAppState = {
|
const exportAppState = {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { defineConfig, devices } from "@playwright/test";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read environment variables from file.
|
||||||
|
* https://github.com/motdotla/dotenv
|
||||||
|
*/
|
||||||
|
// import dotenv from 'dotenv';
|
||||||
|
// import path from 'path';
|
||||||
|
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* See https://playwright.dev/docs/test-configuration.
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: "./excalidraw-app/tests/regression",
|
||||||
|
snapshotPathTemplate:
|
||||||
|
"{snapshotDir}/{testFileDir}/{testFileName}-snapshots/{arg}{ext}",
|
||||||
|
/* Run tests in files in parallel */
|
||||||
|
fullyParallel: true,
|
||||||
|
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
/* Retry on CI only */
|
||||||
|
retries: process.env.CI ? 2 : 1,
|
||||||
|
/* Opt out of parallel tests on CI. */
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
|
reporter: "html",
|
||||||
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
use: {
|
||||||
|
actionTimeout: 0,
|
||||||
|
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||||
|
baseURL: "http://localhost:3000",
|
||||||
|
|
||||||
|
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||||
|
trace: "on-first-retry",
|
||||||
|
headless: true,
|
||||||
|
},
|
||||||
|
timeout: 1200000,
|
||||||
|
|
||||||
|
/* Configure projects for major browsers */
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "chromium",
|
||||||
|
use: {
|
||||||
|
...devices["Desktop Chrome"],
|
||||||
|
launchOptions: {
|
||||||
|
args: ["--disable-font-subpixel-positioning", "--disable-gpu"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// {
|
||||||
|
// name: "firefox",
|
||||||
|
// use: { ...devices["Desktop Firefox"] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
// {
|
||||||
|
// name: "webkit",
|
||||||
|
// use: { ...devices["Desktop Safari"] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against mobile viewports. */
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Chrome',
|
||||||
|
// use: { ...devices['Pixel 5'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Mobile Safari',
|
||||||
|
// use: { ...devices['iPhone 12'] },
|
||||||
|
// },
|
||||||
|
|
||||||
|
/* Test against branded browsers. */
|
||||||
|
// {
|
||||||
|
// name: 'Microsoft Edge',
|
||||||
|
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Google Chrome',
|
||||||
|
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
|
||||||
|
/* Run your local dev server before starting the tests */
|
||||||
|
webServer: {
|
||||||
|
command: "yarn start --no-open",
|
||||||
|
url: "http://localhost:3000",
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
+2
-2
@@ -144,7 +144,7 @@ const askToCommit = (tag, nextVersion) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rl.question(
|
rl.question(
|
||||||
"Do you want to commit these changes to git? (Y/n): ",
|
"Would you like to commit these changes to git? (Y/n): ",
|
||||||
(answer) => {
|
(answer) => {
|
||||||
rl.close();
|
rl.close();
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ const askToPublish = (tag, version) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
rl.question(
|
rl.question(
|
||||||
"Do you want to publish these changes to npm? (Y/n): ",
|
"Would you like to publish these changes to npm? (Y/n): ",
|
||||||
(answer) => {
|
(answer) => {
|
||||||
rl.close();
|
rl.close();
|
||||||
|
|
||||||
|
|||||||
@@ -2162,6 +2162,13 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
|
||||||
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
|
||||||
|
|
||||||
|
"@playwright/test@1.55.0":
|
||||||
|
version "1.55.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.55.0.tgz#080fa6d9ee6d749ff523b1c18259572d0268b963"
|
||||||
|
integrity sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==
|
||||||
|
dependencies:
|
||||||
|
playwright "1.55.0"
|
||||||
|
|
||||||
"@polka/url@^1.0.0-next.24":
|
"@polka/url@^1.0.0-next.24":
|
||||||
version "1.0.0-next.28"
|
version "1.0.0-next.28"
|
||||||
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73"
|
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.28.tgz#d45e01c4a56f143ee69c54dd6b12eade9e270a73"
|
||||||
@@ -2945,6 +2952,13 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
undici-types "~6.20.0"
|
undici-types "~6.20.0"
|
||||||
|
|
||||||
|
"@types/node@24.3.0":
|
||||||
|
version "24.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.3.0.tgz#89b09f45cb9a8ee69466f18ee5864e4c3eb84dec"
|
||||||
|
integrity sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==
|
||||||
|
dependencies:
|
||||||
|
undici-types "~7.10.0"
|
||||||
|
|
||||||
"@types/node@>=12.12.47", "@types/node@>=13.7.0":
|
"@types/node@>=12.12.47", "@types/node@>=13.7.0":
|
||||||
version "22.13.8"
|
version "22.13.8"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.8.tgz#57e2450295b33a6518d6fd4f65f47236d3e41d8d"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.8.tgz#57e2450295b33a6518d6fd4f65f47236d3e41d8d"
|
||||||
@@ -5826,6 +5840,11 @@ fs.realpath@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
||||||
|
|
||||||
|
fsevents@2.3.2:
|
||||||
|
version "2.3.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||||
|
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||||
|
|
||||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||||
version "2.3.3"
|
version "2.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||||
@@ -7838,6 +7857,20 @@ pkg-dir@4.2.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
find-up "^4.0.0"
|
find-up "^4.0.0"
|
||||||
|
|
||||||
|
playwright-core@1.55.0:
|
||||||
|
version "1.55.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.55.0.tgz#ec8a9f8ef118afb3e86e0f46f1393e3bea32adf4"
|
||||||
|
integrity sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==
|
||||||
|
|
||||||
|
playwright@1.55.0:
|
||||||
|
version "1.55.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.55.0.tgz#7aca7ac3ffd9e083a8ad8b2514d6f9ba401cc78b"
|
||||||
|
integrity sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==
|
||||||
|
dependencies:
|
||||||
|
playwright-core "1.55.0"
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents "2.3.2"
|
||||||
|
|
||||||
png-chunk-text@1.0.0:
|
png-chunk-text@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/png-chunk-text/-/png-chunk-text-1.0.0.tgz#1c6006d8e34ba471d38e1c9c54b3f53e1085e18f"
|
resolved "https://registry.yarnpkg.com/png-chunk-text/-/png-chunk-text-1.0.0.tgz#1c6006d8e34ba471d38e1c9c54b3f53e1085e18f"
|
||||||
@@ -9328,6 +9361,11 @@ undici-types@~6.20.0:
|
|||||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
|
||||||
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
|
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
|
||||||
|
|
||||||
|
undici-types@~7.10.0:
|
||||||
|
version "7.10.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.10.0.tgz#4ac2e058ce56b462b056e629cc6a02393d3ff350"
|
||||||
|
integrity sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@^2.0.0:
|
unicode-canonical-property-names-ecmascript@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2"
|
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2"
|
||||||
|
|||||||
Reference in New Issue
Block a user