2026-06-09 08:45:33 +00:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>Excalidraw Editor</title>
|
|
|
|
|
<style>
|
|
|
|
|
* { margin: 0; padding: 0; }
|
|
|
|
|
html, body { width: 100%; height: 100%; overflow: hidden; }
|
|
|
|
|
#root { width: 100%; height: 100%; }
|
2026-07-02 14:29:10 +00:00
|
|
|
.top-right-ui {
|
2026-06-09 08:45:33 +00:00
|
|
|
display: flex;
|
2026-07-02 14:29:10 +00:00
|
|
|
align-items: center;
|
2026-06-09 08:45:33 +00:00
|
|
|
gap: 8px;
|
2026-07-02 14:29:10 +00:00
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
|
|
|
}
|
|
|
|
|
.top-right-ui a, .top-right-ui button {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 6px;
|
2026-06-09 08:45:33 +00:00
|
|
|
padding: 8px 12px;
|
2026-07-02 14:29:10 +00:00
|
|
|
border: 1px solid transparent;
|
2026-06-09 08:45:33 +00:00
|
|
|
border-radius: 8px;
|
|
|
|
|
cursor: pointer;
|
2026-07-02 14:29:10 +00:00
|
|
|
font-size: 13px;
|
2026-06-09 08:45:33 +00:00
|
|
|
text-decoration: none;
|
2026-07-02 14:29:10 +00:00
|
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.12);
|
|
|
|
|
max-width: 40vw;
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
}
|
|
|
|
|
.top-right-ui.theme-light a, .top-right-ui.theme-light button {
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
color: #1b1b1f;
|
2026-06-09 08:45:33 +00:00
|
|
|
}
|
2026-07-02 14:29:10 +00:00
|
|
|
.top-right-ui.theme-dark a, .top-right-ui.theme-dark button {
|
|
|
|
|
background: #232329;
|
|
|
|
|
color: #e9ecef;
|
2026-06-09 08:45:33 +00:00
|
|
|
}
|
2026-07-02 14:29:10 +00:00
|
|
|
.top-right-ui button:hover, .top-right-ui a:hover { border-color: #a29bfe; }
|
2026-06-09 08:45:33 +00:00
|
|
|
.status {
|
|
|
|
|
position: fixed;
|
|
|
|
|
bottom: 10px;
|
2026-07-02 14:29:10 +00:00
|
|
|
right: 60px;
|
2026-06-09 08:45:33 +00:00
|
|
|
padding: 6px 12px;
|
|
|
|
|
background: rgba(0,0,0,0.7);
|
|
|
|
|
color: white;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.3s;
|
2026-07-02 14:29:10 +00:00
|
|
|
pointer-events: none;
|
2026-06-09 08:45:33 +00:00
|
|
|
}
|
|
|
|
|
.status.show { opacity: 1; }
|
|
|
|
|
.loading {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
height: 100%;
|
|
|
|
|
font-size: 1.2rem;
|
|
|
|
|
color: #666;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: 1rem;
|
|
|
|
|
}
|
|
|
|
|
.error { color: #e74c3c; }
|
|
|
|
|
</style>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
<div id="root">
|
|
|
|
|
<div class="loading">
|
|
|
|
|
<div>Loading Excalidraw...</div>
|
|
|
|
|
<div id="load-status" style="font-size: 0.9rem; color: #888;"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="status" class="status">Saved</div>
|
|
|
|
|
|
|
|
|
|
<script>
|
2026-07-02 14:29:10 +00:00
|
|
|
// Replaces #root with an error panel (safe DOM methods, no innerHTML).
|
|
|
|
|
function showFatal(title, detail) {
|
|
|
|
|
var root = document.getElementById('root');
|
|
|
|
|
root.replaceChildren();
|
|
|
|
|
var panel = document.createElement('div');
|
|
|
|
|
panel.className = 'loading error';
|
|
|
|
|
var titleEl = document.createElement('div');
|
|
|
|
|
titleEl.textContent = title;
|
|
|
|
|
panel.appendChild(titleEl);
|
|
|
|
|
if (detail) {
|
|
|
|
|
var detailEl = document.createElement('div');
|
|
|
|
|
detailEl.style.fontSize = '0.9rem';
|
|
|
|
|
detailEl.textContent = detail;
|
|
|
|
|
panel.appendChild(detailEl);
|
|
|
|
|
}
|
|
|
|
|
root.appendChild(panel);
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 08:45:33 +00:00
|
|
|
// Get drawing ID from URL path: /draw/{id}
|
|
|
|
|
var pathParts = window.location.pathname.split('/');
|
|
|
|
|
var drawingId = pathParts[pathParts.length - 1] || pathParts[pathParts.length - 2];
|
|
|
|
|
|
|
|
|
|
if (!drawingId) {
|
2026-07-02 14:29:10 +00:00
|
|
|
showFatal('No drawing ID specified');
|
2026-06-09 08:45:33 +00:00
|
|
|
throw new Error('No drawing ID');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.title = drawingId + ' - Excalidraw';
|
|
|
|
|
|
|
|
|
|
var excalidrawAPI = null;
|
|
|
|
|
var autoSaveTimeout = null;
|
|
|
|
|
|
|
|
|
|
function updateLoadStatus(msg) {
|
|
|
|
|
var el = document.getElementById('load-status');
|
|
|
|
|
if (el) el.textContent = msg;
|
|
|
|
|
console.log('[Excalidraw]', msg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showStatus(msg) {
|
|
|
|
|
var el = document.getElementById('status');
|
|
|
|
|
el.textContent = msg;
|
|
|
|
|
el.classList.add('show');
|
|
|
|
|
setTimeout(function() { el.classList.remove('show'); }, 2000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadDrawing() {
|
|
|
|
|
try {
|
|
|
|
|
var resp = await fetch('/api/drawings/' + drawingId);
|
|
|
|
|
if (resp.ok) {
|
|
|
|
|
return await resp.json();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.log('No existing drawing, starting fresh');
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveDrawing() {
|
|
|
|
|
if (!excalidrawAPI) {
|
|
|
|
|
console.log('Cannot save: excalidrawAPI not ready');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var elements = excalidrawAPI.getSceneElements();
|
|
|
|
|
var appState = excalidrawAPI.getAppState();
|
|
|
|
|
|
|
|
|
|
var data = {
|
|
|
|
|
type: "excalidraw",
|
|
|
|
|
version: 2,
|
|
|
|
|
source: "excalidraw-library",
|
|
|
|
|
elements: elements,
|
|
|
|
|
appState: {
|
|
|
|
|
viewBackgroundColor: appState.viewBackgroundColor,
|
|
|
|
|
gridSize: appState.gridSize
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await fetch('/api/drawings/' + drawingId, {
|
|
|
|
|
method: 'PUT',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(data)
|
|
|
|
|
});
|
|
|
|
|
showStatus('Saved');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
showStatus('Save failed!');
|
|
|
|
|
console.error('Save error:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onChange() {
|
|
|
|
|
clearTimeout(autoSaveTimeout);
|
|
|
|
|
autoSaveTimeout = setTimeout(saveDrawing, 2000);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-02 14:29:10 +00:00
|
|
|
// Renames the current drawing via the API. Returns the new ID, or null
|
|
|
|
|
// if the rename was cancelled or failed.
|
|
|
|
|
async function renameCurrentDrawing() {
|
|
|
|
|
var newName = window.prompt('Rename drawing', drawingId);
|
|
|
|
|
if (newName === null) return null;
|
|
|
|
|
newName = newName.trim();
|
|
|
|
|
if (!newName || newName === drawingId) return null;
|
|
|
|
|
|
|
|
|
|
// A pending autosave would resurrect the old file after the rename.
|
|
|
|
|
clearTimeout(autoSaveTimeout);
|
|
|
|
|
|
|
|
|
|
var resp;
|
|
|
|
|
try {
|
|
|
|
|
resp = await fetch('/api/drawings/' + drawingId, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({ name: newName })
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
showStatus('Rename failed!');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (resp.status === 409) {
|
|
|
|
|
window.alert('A drawing with that name already exists.');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
if (!resp.ok) {
|
|
|
|
|
window.alert('Rename failed: ' + (await resp.text()));
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
var result = await resp.json();
|
|
|
|
|
drawingId = result.id;
|
|
|
|
|
document.title = drawingId + ' - Excalidraw';
|
|
|
|
|
window.history.replaceState(null, '', '/draw/' + encodeURIComponent(drawingId));
|
|
|
|
|
showStatus('Renamed');
|
|
|
|
|
// Flush any unsaved changes to the new file.
|
|
|
|
|
saveDrawing();
|
|
|
|
|
return drawingId;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 08:45:33 +00:00
|
|
|
// Load scripts dynamically
|
|
|
|
|
function loadScript(src) {
|
|
|
|
|
return new Promise(function(resolve, reject) {
|
|
|
|
|
var script = document.createElement('script');
|
|
|
|
|
script.src = src;
|
|
|
|
|
script.crossOrigin = 'anonymous';
|
|
|
|
|
script.onload = resolve;
|
|
|
|
|
script.onerror = function() { reject(new Error('Failed to load: ' + src)); };
|
|
|
|
|
document.head.appendChild(script);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function init() {
|
|
|
|
|
try {
|
|
|
|
|
updateLoadStatus('Loading React...');
|
|
|
|
|
await loadScript('https://unpkg.com/react@18.2.0/umd/react.production.min.js');
|
|
|
|
|
|
|
|
|
|
updateLoadStatus('Loading ReactDOM...');
|
|
|
|
|
await loadScript('https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js');
|
|
|
|
|
|
|
|
|
|
updateLoadStatus('Loading Excalidraw...');
|
|
|
|
|
await loadScript('https://unpkg.com/@excalidraw/excalidraw@0.17.6/dist/excalidraw.production.min.js');
|
|
|
|
|
|
|
|
|
|
updateLoadStatus('Initializing...');
|
|
|
|
|
|
|
|
|
|
// Verify libraries loaded
|
|
|
|
|
if (!window.React) throw new Error('React not loaded');
|
|
|
|
|
if (!window.ReactDOM) throw new Error('ReactDOM not loaded');
|
|
|
|
|
if (!window.ExcalidrawLib) throw new Error('ExcalidrawLib not loaded');
|
|
|
|
|
|
|
|
|
|
console.log('React version:', React.version);
|
|
|
|
|
console.log('ExcalidrawLib:', Object.keys(ExcalidrawLib));
|
|
|
|
|
|
|
|
|
|
updateLoadStatus('Loading drawing data...');
|
|
|
|
|
var initialData = await loadDrawing();
|
|
|
|
|
|
|
|
|
|
updateLoadStatus('Rendering Excalidraw...');
|
|
|
|
|
|
2026-07-02 14:29:10 +00:00
|
|
|
var e = React.createElement;
|
|
|
|
|
var MainMenu = ExcalidrawLib.MainMenu;
|
|
|
|
|
|
|
|
|
|
// Native default menu items, existence-guarded so a library
|
|
|
|
|
// update that drops one degrades gracefully.
|
|
|
|
|
function defaultItem(name) {
|
|
|
|
|
var C = MainMenu && MainMenu.DefaultItems && MainMenu.DefaultItems[name];
|
|
|
|
|
return C ? e(C, { key: name }) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-09 08:45:33 +00:00
|
|
|
function App() {
|
2026-07-02 14:29:10 +00:00
|
|
|
var nameState = React.useState(drawingId);
|
|
|
|
|
var name = nameState[0], setName = nameState[1];
|
|
|
|
|
|
|
|
|
|
function onRename() {
|
|
|
|
|
renameCurrentDrawing().then(function(newId) {
|
|
|
|
|
if (newId) setName(newId);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The menu is where the native export features live:
|
|
|
|
|
// Export = "Save to..." (.excalidraw), SaveAsImage =
|
|
|
|
|
// "Export image..." (PNG / SVG / clipboard).
|
|
|
|
|
var menu = MainMenu ? e(MainMenu, { key: 'menu' },
|
|
|
|
|
e(MainMenu.Item, { key: 'back', onSelect: function() { window.location.href = '/'; } }, 'Back to Library'),
|
|
|
|
|
e(MainMenu.Item, { key: 'save', onSelect: saveDrawing }, 'Save now'),
|
|
|
|
|
e(MainMenu.Item, { key: 'rename', onSelect: onRename }, 'Rename drawing…'),
|
|
|
|
|
MainMenu.Separator ? e(MainMenu.Separator, { key: 'sep1' }) : null,
|
|
|
|
|
defaultItem('LoadScene'),
|
|
|
|
|
defaultItem('Export'),
|
|
|
|
|
defaultItem('SaveAsImage'),
|
|
|
|
|
MainMenu.Separator ? e(MainMenu.Separator, { key: 'sep2' }) : null,
|
|
|
|
|
defaultItem('ClearCanvas'),
|
|
|
|
|
defaultItem('ToggleTheme'),
|
|
|
|
|
defaultItem('ChangeCanvasBackground'),
|
|
|
|
|
defaultItem('Help')
|
|
|
|
|
) : null;
|
|
|
|
|
|
|
|
|
|
return e(ExcalidrawLib.Excalidraw, {
|
2026-06-09 08:45:33 +00:00
|
|
|
initialData: initialData ? {
|
|
|
|
|
elements: initialData.elements || [],
|
|
|
|
|
appState: initialData.appState || {}
|
|
|
|
|
} : undefined,
|
2026-07-02 14:29:10 +00:00
|
|
|
UIOptions: { canvasActions: { toggleTheme: true } },
|
2026-06-09 08:45:33 +00:00
|
|
|
excalidrawAPI: function(api) {
|
|
|
|
|
excalidrawAPI = api;
|
|
|
|
|
console.log('Excalidraw API ready');
|
|
|
|
|
},
|
2026-07-02 14:29:10 +00:00
|
|
|
onChange: onChange,
|
|
|
|
|
renderTopRightUI: function(isMobile, appState) {
|
|
|
|
|
return e('div', { className: 'top-right-ui theme-' + (appState.theme || 'light') },
|
|
|
|
|
e('a', { key: 'home', href: '/', title: 'Back to Library' }, '← Library'),
|
|
|
|
|
e('button', {
|
|
|
|
|
key: 'name',
|
|
|
|
|
title: 'Click to rename',
|
|
|
|
|
onClick: onRename
|
|
|
|
|
}, name + ' ✎')
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}, menu);
|
2026-06-09 08:45:33 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var root = ReactDOM.createRoot(document.getElementById('root'));
|
2026-07-02 14:29:10 +00:00
|
|
|
root.render(e(App));
|
2026-06-09 08:45:33 +00:00
|
|
|
|
|
|
|
|
console.log('Excalidraw rendered successfully');
|
|
|
|
|
|
2026-07-02 14:29:10 +00:00
|
|
|
} catch (err) {
|
|
|
|
|
console.error('Init error:', err);
|
|
|
|
|
showFatal('Failed to load Excalidraw', err.message);
|
2026-06-09 08:45:33 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Keyboard shortcut: Ctrl+S to save
|
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
|
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
saveDrawing();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
init();
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|