跳到主要内容

Draggable resizable modals

Embed DocSpace inside draggable and resizable modal windows built with Vite, React, TypeScript, and Tailwind CSS. This example shows how to embed multiple DocSpace instances, automatically log in via the SDK, and manage them dynamically.

The DocSpace component logs in through a service frame and then opens the file manager.

Before you start​

The app runs on the Vite dev server at http://localhost:5173. Add this origin to the Developer Tools section of DocSpace.

Script execution steps​

1. Create the project​

Create a new React + TypeScript project with Vite and move into it:

npm create vite@latest docspace-modals -- --template react-ts
cd docspace-modals
npm install

The current Vite template requires Node.js 20.19+ or 22.12+.

2. Prepare environment variables​

Create a .env file in the project root:

VITE_DOCSPACE_URL={PORTAL_SRC}
VITE_DOCSPACE_USER_LOGIN=user@example.com
VITE_DOCSPACE_USER_PASSWORD_HASH=PASTE_HASH_HERE
  • VITE_DOCSPACE_URL - your DocSpace portal URL (root).
  • VITE_DOCSPACE_USER_LOGIN - user login for authentication.
  • VITE_DOCSPACE_USER_PASSWORD_HASH - password hash generated via the SDK (see the Create password hash sample).
警告

Vite embeds every VITE_* variable into the client bundle, so anyone who opens the page can read the password hash. Use this approach for local demos only. In production, get the credentials or a token on your backend.

3. Install Tailwind CSS (v3) and configure PostCSS​

  1. Install Tailwind v3 with PostCSS:
npm i -D tailwindcss@3.4.14 postcss@8 autoprefixer@10
  1. Create postcss.config.js in the project root:
// postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
  1. Create tailwind.config.js in the project root:
/** @type {import('tailwindcss').Config} */
export default {
content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"],
theme: { extend: {} },
plugins: [],
};
  1. Replace src/index.css content (create the file if missing):
@tailwind base;
@tailwind components;
@tailwind utilities;

html, body, #root { height: 100%; margin: 0; }

4. Install the DocSpace SDK packages​

Install the React wrapper and the core SDK:

npm i @onlyoffice/docspace-react @onlyoffice/docspace-sdk-js
  • @onlyoffice/docspace-react mounts the iframe, wires SDK events, and pulls in the core SDK at runtime.
  • @onlyoffice/docspace-sdk-js is installed explicitly only to import the SDKInstance and TFrameConfig types. Without TypeScript, the React wrapper alone is enough.

5. Create the modal component​

Add a reusable modal that embeds DocSpace in manager mode. The DocSpace component handles the login itself:

  • email and onRequestPasswordHash tell the component which credentials to use. It opens a service frame in system mode, signs in if the user isn't signed in yet, and then opens the frame with the passed config.
  • onUnsuccessLogin is called if the sign-in fails, for example, because of a wrong password hash.
  • onSetDocspaceInstance passes the SDK instance, which you can use to call SDK methods.

The config has no src because the component takes it from the url prop. It's typed as TFrameConfig, so TypeScript accepts the mode value.

src/components/DraggableModal.tsx

Create the modal component
import { useEffect, useMemo, useRef, useState } from "react";
import { DocSpace } from "@onlyoffice/docspace-react";
import type { SDKInstance, TFrameConfig } from "@onlyoffice/docspace-sdk-js";

interface DraggableModalProps {
id: string;
title: string;
zIndex: number;
portalUrl: string;
login: string;
passwordHash: string;
onClose: (id: string) => void;
onFocus: (id: string) => void;
}

export default function DraggableModal({
id, title, zIndex, portalUrl, login, passwordHash, onClose, onFocus,
}: DraggableModalProps) {
const [pos, setPos] = useState({ x: 80, y: 80 });
const [size, setSize] = useState({ width: 520, height: 380 });
const [drag, setDrag] = useState<{ x: number; y: number } | null>(null);
const [resize, setResize] = useState<{ x: number; y: number } | null>(null);
const frameId = `frame-${id}`;
const instance = useRef<SDKInstance | null>(null);

// drag & resize
useEffect(() => {
const onMove = (e: MouseEvent) => {
if (drag) {
setPos(p => ({ x: p.x + (e.clientX - drag.x), y: p.y + (e.clientY - drag.y) }));
setDrag({ x: e.clientX, y: e.clientY });
}
if (resize) {
setSize(s => ({
width: Math.max(320, s.width + (e.clientX - resize.x)),
height: Math.max(200, s.height + (e.clientY - resize.y)),
}));
setResize({ x: e.clientX, y: e.clientY });
}
};
const onUp = () => { setDrag(null); setResize(null); };
window.addEventListener("mousemove", onMove);
window.addEventListener("mouseup", onUp);
return () => { window.removeEventListener("mousemove", onMove); window.removeEventListener("mouseup", onUp); };
}, [drag, resize]);

// keep the config stable so dragging and resizing don't recreate the frame
const config = useMemo<TFrameConfig>(() => ({
frameId,
width: "100%",
height: "100%",
mode: "manager",
}), [frameId]);

return (
<div
style={{ position: "fixed", top: pos.y, left: pos.x, width: size.width, height: size.height, zIndex }}
className="flex flex-col bg-white border rounded-2xl shadow-lg overflow-hidden"
onMouseDown={() => onFocus(id)}
>
<div
className="flex items-center justify-between bg-[#2e66f5] text-white px-3 py-2 cursor-move"
onMouseDown= {(e) => setDrag({ x: e.clientX, y: e.clientY })}
>
<span className="font-semibold text-sm">{title}</span>
<button
onClick={(e) => { e.stopPropagation(); onClose(id); }}
className="px-2 py-1 rounded hover:bg-[#2556d2]"
aria-label="Close"
>
✕
</button>
</div>

{/* the frame fills the space left under the header */}
<div className="flex-1 min-h-0">
<DocSpace
url={portalUrl}
config={config}
email={login}
onRequestPasswordHash={() => passwordHash}
onUnsuccessLogin={() => console.error(`[${id}] login failed`)}
onSetDocspaceInstance={(ds: SDKInstance) => { instance.current = ds; }}
/>
</div>

<div
className="absolute bottom-0 right-0 w-3 h-3 bg-gray-300 cursor-se-resize"
onMouseDown={(e) => setResize({ x: e.clientX, y: e.clientY })}
title="Resize"
/>
</div>
);
}

6. Render multiple modals​

Create a page that opens, focuses, and closes DocSpace windows.

src/pages/Modals/index.tsx

Render multiple modals
import { useState } from "react";
import DraggableModal from "../../components/DraggableModal";

type Win = { id: string; z: number };

export default function ModalsPage() {
const portal = import.meta.env.VITE_DOCSPACE_URL || "";
const login = import.meta.env.VITE_DOCSPACE_USER_LOGIN || "";
const passwordHash = import.meta.env.VITE_DOCSPACE_USER_PASSWORD_HASH || "";
const [wins, setWins] = useState<Win[]>([{ id: crypto.randomUUID(), z: 10 }]);

function addWin() {
setWins(prev => [...prev, { id: crypto.randomUUID(), z: (prev.at(-1)?.z ?? 10) + 1 }]);
}

function closeWin(id: string) {
setWins(prev => prev.filter(w => w.id !== id));
}

function focusWin(id: string) {
const maxZ = Math.max(...wins.map(w => w.z), 10);
setWins(prev => prev.map(w => (w.id === id ? { ...w, z: maxZ + 1 } : w)));
}

return (
<div className="min-h-screen bg-[#f2f4f7] relative">
<header className="bg-white border-b shadow-sm">
<div className="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
<div>
<h1 className="text-xl font-semibold">DocSpace Modals</h1>
<p className="text-gray-500 text-sm">Draggable & resizable windows with embedded DocSpace.</p>
</div>
<div className="flex items-center gap-2">
{portal ? (
<a href={portal} target="_blank" rel="noreferrer" className="px-3 py-1.5 rounded bg-[#2e66f5] text-white text-sm">
Open DocSpace
</a>
) : (
<span className="text-xs text-red-600">Set VITE_DOCSPACE_URL in .env</span>
)}
<button onClick={addWin} className="px-3 py-1.5 rounded bg-emerald-600 text-white text-sm">
New Window
</button>
</div>
</div>
</header>

<main className="relative min-h-[calc(100vh-56px)]">
{wins.map((w, i) => (
<DraggableModal
key={w.id}
id={w.id}
title={`DocSpace #${i + 1}`}
zIndex={w.z}
portalUrl={portal}
login={login}
passwordHash={passwordHash}
onClose={closeWin}
onFocus={focusWin}
/>
))}
</main>
</div>
);
}

7. Wire the entry point​

Replace src/App.tsx:

import ModalsPage from "./pages/Modals";

export default function App() {
return <ModalsPage />;
}

Make sure src/main.tsx imports ./index.css (the Vite template does this by default).

8. Run the app​

Start the dev server:

npm run dev

Open http://localhost:5173. You should see a modal with DocSpace. Drag it by the header and resize from the bottom-right. Click New Window to open another modal.