Ouverture d'un schéma : cadrage sur l'ensemble du contenu
initialData.scrollToContent centre un seul élément quand le schéma déborde de la fenêtre (calculateScrollCenter → getClosestElementBounds) : le contenu apparaissait à partir du milieu de l'écran. On cadre désormais tout le schéma au premier rendu via scrollToContent(fitToContent), zoom plafonné à 100 %, dans l'éditeur comme dans la vue partagée. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATA96SLvdmiapoLXgenYrR
This commit is contained in:
1 parent
85946ab3a2
commit
af53e30e33
3 files changed
+49
-4
No files matched your search
@@ -23,6 +23,7 @@ import { Splitter } from "./Splitter";
|
|||||||
import { SettingsDialog, DEFAULT_TITLE } from "./SettingsDialog";
|
import { SettingsDialog, DEFAULT_TITLE } from "./SettingsDialog";
|
||||||
import { pendingVersion, useUpdateStatus } from "./update";
|
import { pendingVersion, useUpdateStatus } from "./update";
|
||||||
import { storeThumb } from "./thumbs";
|
import { storeThumb } from "./thumbs";
|
||||||
|
import { fitScene } from "./fit";
|
||||||
|
|
||||||
type Theme = "light" | "dark";
|
type Theme = "light" | "dark";
|
||||||
type SaveState = "saved" | "dirty" | "saving" | "error";
|
type SaveState = "saved" | "dirty" | "saving" | "error";
|
||||||
@@ -105,6 +106,7 @@ export default function App() {
|
|||||||
const libTimer = useRef<number>(undefined);
|
const libTimer = useRef<number>(undefined);
|
||||||
const libJson = useRef(""); // dernier état connu côté serveur
|
const libJson = useRef(""); // dernier état connu côté serveur
|
||||||
const sceneSeq = useRef(0);
|
const sceneSeq = useRef(0);
|
||||||
|
const fitPending = useRef(false); // cadrage à faire au premier rendu du schéma ouvert
|
||||||
const exTheme = useRef<string | null>(null); // dernier thème signalé par Excalidraw
|
const exTheme = useRef<string | null>(null); // dernier thème signalé par Excalidraw
|
||||||
const openRef = useRef<(path: string, opts?: { skipFlush?: boolean }) => Promise<void>>(async () => {});
|
const openRef = useRef<(path: string, opts?: { skipFlush?: boolean }) => Promise<void>>(async () => {});
|
||||||
|
|
||||||
@@ -169,6 +171,10 @@ export default function App() {
|
|||||||
if (appState.theme !== theme) setTheme(appState.theme as Theme);
|
if (appState.theme !== theme) setTheme(appState.theme as Theme);
|
||||||
}
|
}
|
||||||
if (!pathRef.current || appState.isLoading) return;
|
if (!pathRef.current || appState.isLoading) return;
|
||||||
|
if (fitPending.current && excalidraw.current) {
|
||||||
|
fitPending.current = false;
|
||||||
|
fitScene(excalidraw.current);
|
||||||
|
}
|
||||||
const version = sceneVersion(elements, appState, files);
|
const version = sceneVersion(elements, appState, files);
|
||||||
// Premier onChange après chargement : c'est la version de référence.
|
// Premier onChange après chargement : c'est la version de référence.
|
||||||
if (savedVersion.current === "") {
|
if (savedVersion.current === "") {
|
||||||
@@ -195,6 +201,7 @@ export default function App() {
|
|||||||
etagRef.current = etag;
|
etagRef.current = etag;
|
||||||
savedVersion.current = ""; // fixée au premier onChange (voir onChange)
|
savedVersion.current = ""; // fixée au premier onChange (voir onChange)
|
||||||
pathRef.current = path;
|
pathRef.current = path;
|
||||||
|
fitPending.current = true;
|
||||||
setScene({
|
setScene({
|
||||||
id: ++sceneSeq.current,
|
id: ++sceneSeq.current,
|
||||||
path,
|
path,
|
||||||
@@ -202,7 +209,6 @@ export default function App() {
|
|||||||
elements: data.elements ?? [],
|
elements: data.elements ?? [],
|
||||||
appState: { ...(data.appState ?? {}), theme },
|
appState: { ...(data.appState ?? {}), theme },
|
||||||
files: data.files ?? {},
|
files: data.files ?? {},
|
||||||
scrollToContent: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setSaveState("saved");
|
setSaveState("saved");
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { Excalidraw } from "@excalidraw/excalidraw";
|
import { Excalidraw } from "@excalidraw/excalidraw";
|
||||||
import type { ExcalidrawInitialDataState } from "@excalidraw/excalidraw/types";
|
import type { AppState, ExcalidrawImperativeAPI, ExcalidrawInitialDataState } from "@excalidraw/excalidraw/types";
|
||||||
import { api, type PublicScene } from "./api";
|
import { api, type PublicScene } from "./api";
|
||||||
import { DEFAULT_TITLE } from "./SettingsDialog";
|
import { DEFAULT_TITLE } from "./SettingsDialog";
|
||||||
|
import { fitScene } from "./fit";
|
||||||
import { DownloadIcon, EyeIcon, MoonIcon, SunIcon } from "./icons";
|
import { DownloadIcon, EyeIcon, MoonIcon, SunIcon } from "./icons";
|
||||||
|
|
||||||
type Theme = "light" | "dark";
|
type Theme = "light" | "dark";
|
||||||
@@ -22,6 +23,21 @@ export function ShareView({ token }: { token: string }) {
|
|||||||
const [data, setData] = useState<PublicScene | null>(null);
|
const [data, setData] = useState<PublicScene | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [theme, setTheme] = useState<Theme>(initialTheme);
|
const [theme, setTheme] = useState<Theme>(initialTheme);
|
||||||
|
const excalidraw = useRef<ExcalidrawImperativeAPI | null>(null);
|
||||||
|
const fitted = useRef(false);
|
||||||
|
|
||||||
|
const onApi = useCallback((ex: ExcalidrawImperativeAPI | null) => {
|
||||||
|
excalidraw.current = ex;
|
||||||
|
}, []);
|
||||||
|
// excalidrawAPI jusqu'à la 0.18, onExcalidrawAPI sur le canal next.
|
||||||
|
const apiProps = { excalidrawAPI: onApi, onExcalidrawAPI: onApi } as object;
|
||||||
|
|
||||||
|
// Premier rendu : cadrage sur l'ensemble du schéma.
|
||||||
|
const onChange = useCallback((_elements: unknown, appState: AppState) => {
|
||||||
|
if (appState.isLoading || fitted.current || !excalidraw.current) return;
|
||||||
|
fitted.current = true;
|
||||||
|
fitScene(excalidraw.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api
|
api
|
||||||
@@ -51,7 +67,6 @@ export function ShareView({ token }: { token: string }) {
|
|||||||
elements: (data.scene.elements ?? []) as ExcalidrawInitialDataState["elements"],
|
elements: (data.scene.elements ?? []) as ExcalidrawInitialDataState["elements"],
|
||||||
appState: { ...(data.scene.appState ?? {}), theme } as ExcalidrawInitialDataState["appState"],
|
appState: { ...(data.scene.appState ?? {}), theme } as ExcalidrawInitialDataState["appState"],
|
||||||
files: (data.scene.files ?? {}) as ExcalidrawInitialDataState["files"],
|
files: (data.scene.files ?? {}) as ExcalidrawInitialDataState["files"],
|
||||||
scrollToContent: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -96,6 +111,8 @@ export function ShareView({ token }: { token: string }) {
|
|||||||
<Excalidraw
|
<Excalidraw
|
||||||
initialData={initialData}
|
initialData={initialData}
|
||||||
viewModeEnabled
|
viewModeEnabled
|
||||||
|
{...apiProps}
|
||||||
|
onChange={onChange}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
langCode="fr-FR"
|
langCode="fr-FR"
|
||||||
name={data!.name}
|
name={data!.name}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { ExcalidrawImperativeAPI } from "@excalidraw/excalidraw/types";
|
||||||
|
|
||||||
|
const isMobile = () => window.matchMedia("(max-width: 768px)").matches;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cadre tout le schéma dans la zone visible, sans zoomer au-delà de 100 %.
|
||||||
|
*
|
||||||
|
* Remplace `initialData.scrollToContent` : quand le contenu déborde de la
|
||||||
|
* fenêtre, Excalidraw centre alors un seul élément (le plus proche de l'origine)
|
||||||
|
* au lieu de l'ensemble, et le schéma s'affiche à partir du milieu de l'écran.
|
||||||
|
*/
|
||||||
|
export function fitScene(ex: ExcalidrawImperativeAPI) {
|
||||||
|
const elements = ex.getSceneElements();
|
||||||
|
if (elements.length === 0) return;
|
||||||
|
ex.scrollToContent(elements, {
|
||||||
|
fitToContent: true,
|
||||||
|
viewportZoomFactor: 0.9,
|
||||||
|
animate: false,
|
||||||
|
// Barres d'outils d'Excalidraw posées sur le canevas (en haut, et en bas sur mobile).
|
||||||
|
canvasOffsets: { top: 64, bottom: isMobile() ? 64 : 0 },
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user