infra/stacks/excalidraw/project/static/editor.html

338 lines
13 KiB
HTML
Raw Normal View History

<!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%; }
.top-right-ui {
display: flex;
align-items: center;
gap: 8px;
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;
padding: 8px 12px;
border: 1px solid transparent;
border-radius: 8px;
cursor: pointer;
font-size: 13px;
text-decoration: none;
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;
}
.top-right-ui.theme-dark a, .top-right-ui.theme-dark button {
background: #232329;
color: #e9ecef;
}
.top-right-ui button:hover, .top-right-ui a:hover { border-color: #a29bfe; }
.status {
position: fixed;
bottom: 10px;
right: 60px;
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;
pointer-events: none;
}
.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>
// 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);
}
// 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) {
showFatal('No drawing ID specified');
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);
}
// 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;
}
// 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...');
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;
}
function App() {
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, {
initialData: initialData ? {
elements: initialData.elements || [],
appState: initialData.appState || {}
} : undefined,
UIOptions: { canvasActions: { toggleTheme: true } },
excalidrawAPI: function(api) {
excalidrawAPI = api;
console.log('Excalidraw API ready');
},
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);
}
var root = ReactDOM.createRoot(document.getElementById('root'));
root.render(e(App));
console.log('Excalidraw rendered successfully');
} catch (err) {
console.error('Init error:', err);
showFatal('Failed to load Excalidraw', err.message);
}
}
// 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>