mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-11-01 03:14:29 +01:00
Compare commits
1 Commits
fix-zsvicz
...
ryan-di/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e625d5aba3 |
@@ -2,9 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>
|
||||
Free, collaborative whiteboard • Hand-drawn look & feel | Excalidraw
|
||||
</title>
|
||||
<title>Excalidraw | Hand-drawn look & feel • Collaborative • Secure</title>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover, shrink-to-fit=no"
|
||||
@@ -16,7 +14,7 @@
|
||||
<!-- Primary Meta Tags -->
|
||||
<meta
|
||||
name="title"
|
||||
content="Free, collaborative whiteboard • Hand-drawn look & feel | Excalidraw"
|
||||
content="Excalidraw — Collaborative whiteboarding made easy"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
|
||||
@@ -151,16 +151,6 @@ export class Delta<T> {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two deltas into a new one.
|
||||
*/
|
||||
public static merge<T>(delta1: Delta<T>, delta2: Delta<T>) {
|
||||
return Delta.create(
|
||||
{ ...delta1.deleted, ...delta2.deleted },
|
||||
{ ...delta1.inserted, ...delta2.inserted },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges deleted and inserted object partials.
|
||||
*/
|
||||
@@ -507,11 +497,6 @@ export interface DeltaContainer<T> {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -519,7 +504,7 @@ export interface DeltaContainer<T> {
|
||||
}
|
||||
|
||||
export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
private constructor(public delta: Delta<ObservedAppState>) {}
|
||||
private constructor(public readonly delta: Delta<ObservedAppState>) {}
|
||||
|
||||
public static calculate<T extends ObservedAppState>(
|
||||
prevAppState: T,
|
||||
@@ -550,11 +535,6 @@ export class AppStateDelta implements DeltaContainer<AppState> {
|
||||
return new AppStateDelta(inversedDelta);
|
||||
}
|
||||
|
||||
public squash(delta: AppStateDelta): this {
|
||||
this.delta = Delta.merge(this.delta, delta.delta);
|
||||
return this;
|
||||
}
|
||||
|
||||
public applyTo(
|
||||
appState: AppState,
|
||||
nextElements: SceneElementsMap,
|
||||
@@ -1216,8 +1196,8 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
const inverseInternal = (deltas: Record<string, Delta<ElementPartial>>) => {
|
||||
const inversedDeltas: Record<string, Delta<ElementPartial>> = {};
|
||||
|
||||
for (const [id, { inserted, deleted }] of Object.entries(deltas)) {
|
||||
inversedDeltas[id] = Delta.create({ ...inserted }, { ...deleted });
|
||||
for (const [id, delta] of Object.entries(deltas)) {
|
||||
inversedDeltas[id] = Delta.create(delta.inserted, delta.deleted);
|
||||
}
|
||||
|
||||
return inversedDeltas;
|
||||
@@ -1415,42 +1395,6 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
}
|
||||
}
|
||||
|
||||
public squash(delta: ElementsDelta): this {
|
||||
const { added, removed, updated } = delta;
|
||||
|
||||
for (const [id, nextDelta] of Object.entries(added)) {
|
||||
const prevDelta = this.added[id];
|
||||
|
||||
if (!prevDelta) {
|
||||
this.added[id] = nextDelta;
|
||||
} else {
|
||||
this.added[id] = Delta.merge(prevDelta, nextDelta);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, nextDelta] of Object.entries(removed)) {
|
||||
const prevDelta = this.removed[id];
|
||||
|
||||
if (!prevDelta) {
|
||||
this.removed[id] = nextDelta;
|
||||
} else {
|
||||
this.removed[id] = Delta.merge(prevDelta, nextDelta);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [id, nextDelta] of Object.entries(updated)) {
|
||||
const prevDelta = this.updated[id];
|
||||
|
||||
if (!prevDelta) {
|
||||
this.updated[id] = nextDelta;
|
||||
} else {
|
||||
this.updated[id] = Delta.merge(prevDelta, nextDelta);
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private static createApplier =
|
||||
(
|
||||
nextElements: SceneElementsMap,
|
||||
@@ -1680,12 +1624,25 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
|
||||
Array.from(prevElements).filter(([id]) => nextAffectedElements.has(id)),
|
||||
);
|
||||
|
||||
// calculate complete deltas for affected elements, and squash them back to the current deltas
|
||||
this.squash(
|
||||
// technically we could do better here if perf. would become an issue
|
||||
ElementsDelta.calculate(prevAffectedElements, nextAffectedElements),
|
||||
// calculate complete deltas for affected elements, and assign them back to all the deltas
|
||||
// technically we could do better here if perf. would become an issue
|
||||
const { added, removed, updated } = ElementsDelta.calculate(
|
||||
prevAffectedElements,
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -76,9 +76,8 @@ type MicroActionsQueue = (() => void)[];
|
||||
* Store which captures the observed changes and emits them as `StoreIncrement` events.
|
||||
*/
|
||||
export class Store {
|
||||
// for internal use by history
|
||||
// internally used by history
|
||||
public readonly onDurableIncrementEmitter = new Emitter<[DurableIncrement]>();
|
||||
// for public use as part of onIncrement API
|
||||
public readonly onStoreIncrementEmitter = new Emitter<
|
||||
[DurableIncrement | EphemeralIncrement]
|
||||
>();
|
||||
@@ -240,6 +239,7 @@ export class Store {
|
||||
if (!storeDelta.isEmpty()) {
|
||||
const increment = new DurableIncrement(storeChange, storeDelta);
|
||||
|
||||
// Notify listeners with the increment
|
||||
this.onDurableIncrementEmitter.trigger(increment);
|
||||
this.onStoreIncrementEmitter.trigger(increment);
|
||||
}
|
||||
|
||||
@@ -9107,7 +9107,7 @@ class App extends React.Component<AppProps, AppState> {
|
||||
newElement &&
|
||||
!multiElement
|
||||
) {
|
||||
if (this.device.isTouchScreen && newElement!.points.length > 1) {
|
||||
if (this.device.isTouchScreen) {
|
||||
const FIXED_DELTA_X = Math.min(
|
||||
(this.state.width * 0.7) / this.state.zoom.value,
|
||||
100,
|
||||
|
||||
@@ -4,7 +4,13 @@ import {
|
||||
supported as nativeFileSystemSupported,
|
||||
} from "browser-fs-access";
|
||||
|
||||
import { EVENT, MIME_TYPES, debounce } from "@excalidraw/common";
|
||||
import {
|
||||
EVENT,
|
||||
MIME_TYPES,
|
||||
debounce,
|
||||
isIOS,
|
||||
isAndroid,
|
||||
} from "@excalidraw/common";
|
||||
|
||||
import { AbortError } from "../errors";
|
||||
|
||||
@@ -13,6 +19,8 @@ import type { FileSystemHandle } from "browser-fs-access";
|
||||
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||
|
||||
const INPUT_CHANGE_INTERVAL_MS = 500;
|
||||
// increase timeout for mobile devices to give more time for file selection
|
||||
const MOBILE_INPUT_CHANGE_INTERVAL_MS = 2000;
|
||||
|
||||
export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
||||
extensions?: FILE_EXTENSION[];
|
||||
@@ -41,13 +49,22 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
||||
mimeTypes,
|
||||
multiple: opts.multiple ?? false,
|
||||
legacySetup: (resolve, reject, input) => {
|
||||
const scheduleRejection = debounce(reject, INPUT_CHANGE_INTERVAL_MS);
|
||||
const isMobile = isIOS || isAndroid;
|
||||
const intervalMs = isMobile
|
||||
? MOBILE_INPUT_CHANGE_INTERVAL_MS
|
||||
: INPUT_CHANGE_INTERVAL_MS;
|
||||
const scheduleRejection = debounce(reject, intervalMs);
|
||||
|
||||
const focusHandler = () => {
|
||||
checkForFile();
|
||||
document.addEventListener(EVENT.KEYUP, scheduleRejection);
|
||||
document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
|
||||
scheduleRejection();
|
||||
// on mobile, be less aggressive with rejection
|
||||
if (!isMobile) {
|
||||
document.addEventListener(EVENT.KEYUP, scheduleRejection);
|
||||
document.addEventListener(EVENT.POINTER_UP, scheduleRejection);
|
||||
scheduleRejection();
|
||||
}
|
||||
};
|
||||
|
||||
const checkForFile = () => {
|
||||
// this hack might not work when expecting multiple files
|
||||
if (input.files?.length) {
|
||||
@@ -55,12 +72,15 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
||||
resolve(ret as RetType);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
window.addEventListener(EVENT.FOCUS, focusHandler);
|
||||
});
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
checkForFile();
|
||||
}, INPUT_CHANGE_INTERVAL_MS);
|
||||
}, intervalMs);
|
||||
|
||||
return (rejectPromise) => {
|
||||
clearInterval(interval);
|
||||
scheduleRejection.cancel();
|
||||
@@ -69,7 +89,9 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
||||
document.removeEventListener(EVENT.POINTER_UP, scheduleRejection);
|
||||
if (rejectPromise) {
|
||||
// so that something is shown in console if we need to debug this
|
||||
console.warn("Opening the file was canceled (legacy-fs).");
|
||||
console.warn(
|
||||
"Opening the file was canceled (legacy-fs). This may happen on mobile devices.",
|
||||
);
|
||||
rejectPromise(new AbortError());
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2924,7 +2924,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"version": 7,
|
||||
"width": 100,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@@ -3001,7 +3001,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"version": 5,
|
||||
"verticalAlign": "top",
|
||||
"width": 30,
|
||||
"x": 15,
|
||||
@@ -3031,67 +3031,14 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"version": 9,
|
||||
},
|
||||
"inserted": {
|
||||
"angle": 0,
|
||||
"autoResize": true,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"containerId": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 5,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lineHeight": "1.25000",
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"originalText": "que pasa",
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"text": "que pasa",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"version": 8,
|
||||
"verticalAlign": "top",
|
||||
"width": 100,
|
||||
"x": 15,
|
||||
"y": 15,
|
||||
},
|
||||
},
|
||||
},
|
||||
"removed": {},
|
||||
"updated": {
|
||||
"id0": {
|
||||
"deleted": {
|
||||
"boundElements": [],
|
||||
"version": 11,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
{
|
||||
"id": "id1",
|
||||
"type": "text",
|
||||
},
|
||||
],
|
||||
"version": 10,
|
||||
},
|
||||
},
|
||||
"id5": {
|
||||
"deleted": {
|
||||
"version": 11,
|
||||
},
|
||||
"inserted": {
|
||||
"version": 9,
|
||||
},
|
||||
},
|
||||
},
|
||||
"updated": {},
|
||||
},
|
||||
"id": "id9",
|
||||
},
|
||||
@@ -5089,29 +5036,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"removed": {
|
||||
"id0": {
|
||||
"deleted": {
|
||||
"angle": 0,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": [],
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"type": "rectangle",
|
||||
"version": 8,
|
||||
"width": 100,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
},
|
||||
"inserted": {
|
||||
"boundElements": [
|
||||
@@ -5339,38 +5266,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
||||
"removed": {
|
||||
"id1": {
|
||||
"deleted": {
|
||||
"angle": 0,
|
||||
"autoResize": true,
|
||||
"backgroundColor": "transparent",
|
||||
"boundElements": null,
|
||||
"containerId": null,
|
||||
"customData": undefined,
|
||||
"fillStyle": "solid",
|
||||
"fontFamily": 5,
|
||||
"fontSize": 20,
|
||||
"frameId": null,
|
||||
"groupIds": [],
|
||||
"height": 100,
|
||||
"index": "a0",
|
||||
"isDeleted": false,
|
||||
"lineHeight": "1.25000",
|
||||
"link": null,
|
||||
"locked": false,
|
||||
"opacity": 100,
|
||||
"originalText": "que pasa",
|
||||
"roughness": 1,
|
||||
"roundness": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 2,
|
||||
"text": "que pasa",
|
||||
"textAlign": "left",
|
||||
"type": "text",
|
||||
"version": 8,
|
||||
"verticalAlign": "top",
|
||||
"width": 100,
|
||||
"x": 15,
|
||||
"y": 15,
|
||||
},
|
||||
"inserted": {
|
||||
"containerId": "id0",
|
||||
@@ -5627,11 +5525,9 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
||||
"updated": {
|
||||
"id1": {
|
||||
"deleted": {
|
||||
"frameId": null,
|
||||
"version": 10,
|
||||
},
|
||||
"inserted": {
|
||||
"frameId": null,
|
||||
"version": 8,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -144,7 +144,7 @@ const askToCommit = (tag, nextVersion) => {
|
||||
});
|
||||
|
||||
rl.question(
|
||||
"Would you like to commit these changes to git? (Y/n): ",
|
||||
"Do you want to commit these changes to git? (Y/n): ",
|
||||
(answer) => {
|
||||
rl.close();
|
||||
|
||||
@@ -189,7 +189,7 @@ const askToPublish = (tag, version) => {
|
||||
});
|
||||
|
||||
rl.question(
|
||||
"Would you like to publish these changes to npm? (Y/n): ",
|
||||
"Do you want to publish these changes to npm? (Y/n): ",
|
||||
(answer) => {
|
||||
rl.close();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user