Compare commits

..

3 Commits

Author SHA1 Message Date
Ryan Di
e625d5aba3 fix: extend wait time for file loading on mobile devices 2025-08-01 12:42:20 +10:00
Ryan Di
178eca5828 fix: add frame clipping to new element canvas (#9794)
* fix: add frame clipping to new element canvas

* cleanup save/restore

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2025-07-31 12:10:59 +00:00
Ryan Di
cb33de25f4 feat: allow a frame to snap to its children (#9795) 2025-07-31 13:58:29 +02:00
5 changed files with 62 additions and 43 deletions

View File

@@ -1,23 +0,0 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- uses: actions/stale@v9
with:
days-before-issue-stale: 90
days-before-issue-close: 180
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 90 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 180 days since being marked as stale."
exempt-issue-assignees: "dwelle,ryan-di,Mrazator,ad1992,zsviczian,mtolmacs"
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -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());
}
};

View File

@@ -1,9 +1,16 @@
import { throttleRAF } from "@excalidraw/common";
import { isInvisiblySmallElement, renderElement } from "@excalidraw/element";
import {
getTargetFrame,
isInvisiblySmallElement,
renderElement,
shouldApplyFrameClip,
} from "@excalidraw/element";
import { bootstrapCanvas, getNormalizedCanvasDimensions } from "./helpers";
import { frameClip } from "./staticScene";
import type { NewElementSceneRenderConfig } from "../scene/types";
const _renderNewElementScene = ({
@@ -29,8 +36,9 @@ const _renderNewElementScene = ({
normalizedHeight,
});
// Apply zoom
context.save();
// Apply zoom
context.scale(appState.zoom.value, appState.zoom.value);
if (newElement && newElement.type !== "selection") {
@@ -42,6 +50,23 @@ const _renderNewElementScene = ({
return;
}
const frameId = newElement.frameId || appState.frameToHighlight?.id;
if (
frameId &&
appState.frameRendering.enabled &&
appState.frameRendering.clip
) {
const frame = getTargetFrame(newElement, elementsMap, appState);
if (
frame &&
shouldApplyFrameClip(newElement, frame, appState, elementsMap)
) {
frameClip(frame, context, renderConfig, appState);
}
}
renderElement(
newElement,
elementsMap,
@@ -54,6 +79,8 @@ const _renderNewElementScene = ({
} else {
context.clearRect(0, 0, normalizedWidth, normalizedHeight);
}
context.restore();
}
};

View File

@@ -113,7 +113,7 @@ const strokeGrid = (
context.restore();
};
const frameClip = (
export const frameClip = (
frame: ExcalidrawFrameLikeElement,
context: CanvasRenderingContext2D,
renderConfig: StaticCanvasRenderConfig,

View File

@@ -13,7 +13,7 @@ import {
getDraggedElementsBounds,
getElementAbsoluteCoords,
} from "@excalidraw/element";
import { isBoundToContainer, isFrameLikeElement } from "@excalidraw/element";
import { isBoundToContainer } from "@excalidraw/element";
import { getMaximumGroups } from "@excalidraw/element";
@@ -311,20 +311,13 @@ const getReferenceElements = (
selectedElements: NonDeletedExcalidrawElement[],
appState: AppState,
elementsMap: ElementsMap,
) => {
const selectedFrames = selectedElements
.filter((element) => isFrameLikeElement(element))
.map((frame) => frame.id);
return getVisibleAndNonSelectedElements(
) =>
getVisibleAndNonSelectedElements(
elements,
selectedElements,
appState,
elementsMap,
).filter(
(element) => !(element.frameId && selectedFrames.includes(element.frameId)),
);
};
export const getVisibleGaps = (
elements: readonly NonDeletedExcalidrawElement[],