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 { pendingVersion, useUpdateStatus } from "./update";
|
||||
import { storeThumb } from "./thumbs";
|
||||
import { fitScene } from "./fit";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
type SaveState = "saved" | "dirty" | "saving" | "error";
|
||||
@@ -105,6 +106,7 @@ export default function App() {
|
||||
const libTimer = useRef<number>(undefined);
|
||||
const libJson = useRef(""); // dernier état connu côté serveur
|
||||
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 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 (!pathRef.current || appState.isLoading) return;
|
||||
if (fitPending.current && excalidraw.current) {
|
||||
fitPending.current = false;
|
||||
fitScene(excalidraw.current);
|
||||
}
|
||||
const version = sceneVersion(elements, appState, files);
|
||||
// Premier onChange après chargement : c'est la version de référence.
|
||||
if (savedVersion.current === "") {
|
||||
@@ -195,6 +201,7 @@ export default function App() {
|
||||
etagRef.current = etag;
|
||||
savedVersion.current = ""; // fixée au premier onChange (voir onChange)
|
||||
pathRef.current = path;
|
||||
fitPending.current = true;
|
||||
setScene({
|
||||
id: ++sceneSeq.current,
|
||||
path,
|
||||
@@ -202,7 +209,6 @@ export default function App() {
|
||||
elements: data.elements ?? [],
|
||||
appState: { ...(data.appState ?? {}), theme },
|
||||
files: data.files ?? {},
|
||||
scrollToContent: true,
|
||||
},
|
||||
});
|
||||
setSaveState("saved");
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
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 { DEFAULT_TITLE } from "./SettingsDialog";
|
||||
import { fitScene } from "./fit";
|
||||
import { DownloadIcon, EyeIcon, MoonIcon, SunIcon } from "./icons";
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
@@ -22,6 +23,21 @@ export function ShareView({ token }: { token: string }) {
|
||||
const [data, setData] = useState<PublicScene | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
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(() => {
|
||||
api
|
||||
@@ -51,7 +67,6 @@ export function ShareView({ token }: { token: string }) {
|
||||
elements: (data.scene.elements ?? []) as ExcalidrawInitialDataState["elements"],
|
||||
appState: { ...(data.scene.appState ?? {}), theme } as ExcalidrawInitialDataState["appState"],
|
||||
files: (data.scene.files ?? {}) as ExcalidrawInitialDataState["files"],
|
||||
scrollToContent: true,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -96,6 +111,8 @@ export function ShareView({ token }: { token: string }) {
|
||||
<Excalidraw
|
||||
initialData={initialData}
|
||||
viewModeEnabled
|
||||
{...apiProps}
|
||||
onChange={onChange}
|
||||
theme={theme}
|
||||
langCode="fr-FR"
|
||||
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