CI / lint (push) Successful in 54s
CI / security (push) Successful in 38s
CI / test (push) Successful in 1m7s
CI / build (push) Successful in 33s
CI / e2e (push) Failing after 10m52s
Desktop Build / build-windows (push) Canceled after 0s
Desktop Build / build-linux (push) Canceled after 0s
- frontend/js/search.js : onSearchFilter appelee avec (vault, query, results) — renvoyait undefined, cassait le rendu et basculait sur la recherche offline (0 resultat). Regression plugins depuis 30f2df3.
- frontend/excalidraw-editor.html + js/excalidraw-viewer.js : ignorer onChange au montage (plus d'auto-save spurieux qui reecrivait .excalidraw.md en JSON brut) + preservation du format Obsidian frontmatter + compressed-json a la sauvegarde.
- frontend/js/app.js : __OBSIGATE_BOOTED pose apres init() (splash + E2E attendent les handlers lies).
- tests/e2e : goHome/login attendent le boot ; excalidraw.spec.js reecrit avec les selecteurs reels (modal, context menu, canvas.first()).
- ROADMAP : C8 (creation via menu contextuel) marque fait.
- Verifie : 86/86 E2E chromium-desktop, 599 backend, ruff, validate-imports, JSDOM.
165 lines
5.5 KiB
JavaScript
165 lines
5.5 KiB
JavaScript
/**
|
|
* Excalidraw Viewer — iframe-based diagram editor integration for ObsiGate.
|
|
*
|
|
* Renders .excalidraw files in an iframe that loads the standalone
|
|
* excalidraw-editor.html. Communication between parent and iframe
|
|
* via postMessage:
|
|
*
|
|
* Parent → Iframe: init {data, theme} | theme {theme} | requestSave
|
|
* Iframe → Parent: ready | save {data} | modified {dirty}
|
|
*/
|
|
|
|
import { api } from './auth.js';
|
|
import { showToast } from './ui.js';
|
|
|
|
// Track active Excalidraw iframes by a unique id
|
|
const _activeEditors = new Map();
|
|
let _editorIdCounter = 0;
|
|
|
|
/**
|
|
* Render an Excalidraw editor inside the given container element.
|
|
*
|
|
* @param {HTMLElement} container - DOM element to host the iframe (usually content-area)
|
|
* @param {object} data - File data from /api/file/{vault} (must have is_excalidraw: true)
|
|
* @param {string} vaultName
|
|
* @param {string} filePath
|
|
* @param {object} [opts]
|
|
* @param {boolean} [opts.readOnly=false] - If true, hide save button in iframe
|
|
*/
|
|
export function renderExcalidraw(container, data, vaultName, filePath, opts = {}) {
|
|
const editorId = `excalidraw-${++_editorIdCounter}`;
|
|
|
|
// Build the iframe
|
|
const iframe = document.createElement('iframe');
|
|
iframe.id = editorId;
|
|
iframe.src = '/static/excalidraw-editor.html?v=' + Date.now();
|
|
iframe.sandbox.add('allow-scripts');
|
|
iframe.sandbox.add('allow-same-origin');
|
|
iframe.style.cssText = 'width:100%;height:100%;border:none;';
|
|
|
|
// Clean container and insert iframe
|
|
container.innerHTML = '';
|
|
container.appendChild(iframe);
|
|
|
|
// Track this editor instance
|
|
const editorState = {
|
|
id: editorId,
|
|
iframe,
|
|
vault: vaultName,
|
|
path: filePath,
|
|
isDirty: false,
|
|
ready: false,
|
|
saveTimer: null,
|
|
};
|
|
_activeEditors.set(editorId, editorState);
|
|
|
|
// Get current theme
|
|
const theme = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
|
|
// Wait for iframe to load, then send init
|
|
iframe.addEventListener('load', () => {
|
|
// Send init message when iframe sends 'ready'
|
|
// (iframe may load before React mounts, so we wait for the ready signal)
|
|
// We set up the message listener before sending anything
|
|
});
|
|
|
|
// Handle messages from iframe
|
|
window.addEventListener('message', (event) => {
|
|
// Verify source is our iframe
|
|
if (event.source !== iframe.contentWindow) return;
|
|
|
|
const msg = event.data;
|
|
if (!msg || typeof msg.type !== 'string') return;
|
|
|
|
switch (msg.type) {
|
|
case 'ready':
|
|
// Iframe is mounted — send initial data
|
|
editorState.ready = true;
|
|
// Support both pure .excalidraw and compressed .excalidraw.md
|
|
const initData = data.excalidraw_data || {};
|
|
if (data.excalidraw_data_compressed) {
|
|
initData.compressed = data.excalidraw_data_compressed;
|
|
}
|
|
iframe.contentWindow.postMessage({
|
|
type: 'init',
|
|
data: initData,
|
|
theme: theme,
|
|
}, '*');
|
|
break;
|
|
|
|
case 'save':
|
|
if (msg.data) {
|
|
_saveExcalidrawFile(editorState, msg.data);
|
|
}
|
|
break;
|
|
|
|
case 'modified':
|
|
editorState.isDirty = msg.dirty === true;
|
|
// If dirty, start auto-save timer (2s debounce)
|
|
if (editorState.isDirty) {
|
|
if (editorState.saveTimer) clearTimeout(editorState.saveTimer);
|
|
editorState.saveTimer = setTimeout(() => {
|
|
if (editorState.isDirty && editorState.ready) {
|
|
iframe.contentWindow.postMessage({ type: 'requestSave' }, '*');
|
|
}
|
|
}, 2000);
|
|
}
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Save Excalidraw data back to the server.
|
|
*/
|
|
async function _saveExcalidrawFile(state, excalidrawData) {
|
|
try {
|
|
const isMd = (state.path || '').toLowerCase().endsWith('.excalidraw.md');
|
|
let content;
|
|
if (isMd && excalidrawData && excalidrawData.compressed) {
|
|
// Preserve the Obsidian Excalidraw plugin format: frontmatter +
|
|
// compressed-json block. Writing raw JSON here would destroy the
|
|
// .excalidraw.md file (the plugin would no longer recognize it).
|
|
content = `---\nexcalidraw-plugin: parsed\n---\n\n\`\`\`compressed-json\n${excalidrawData.compressed}\n\`\`\`\n`;
|
|
} else {
|
|
// Transport-only field must not leak into the pure JSON format.
|
|
const { compressed, ...scene } = excalidrawData || {};
|
|
content = JSON.stringify(scene, null, 2);
|
|
}
|
|
await api(`/api/file/${encodeURIComponent(state.vault)}/save?path=${encodeURIComponent(state.path)}&backup=false`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ content }),
|
|
});
|
|
// Toast is handled by the caller or we show a quick one
|
|
showToast('Excalidraw saved', 'success');
|
|
} catch (err) {
|
|
console.error('[Excalidraw] Save failed:', err);
|
|
showToast('Failed to save Excalidraw: ' + (err.message || 'unknown error'), 'error');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify all active Excalidraw editors of a theme change.
|
|
*/
|
|
export function notifyExcalidrawThemeChange(theme) {
|
|
_activeEditors.forEach((state) => {
|
|
if (state.ready && state.iframe.contentWindow) {
|
|
state.iframe.contentWindow.postMessage({ type: 'theme', theme }, '*');
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Clean up an editor instance (e.g., when tab is closed).
|
|
*/
|
|
export function destroyExcalidrawEditor(editorId) {
|
|
const state = _activeEditors.get(editorId);
|
|
if (state) {
|
|
if (state.saveTimer) clearTimeout(state.saveTimer);
|
|
_activeEditors.delete(editorId);
|
|
}
|
|
}
|
|
|
|
export default { renderExcalidraw, notifyExcalidrawThemeChange, destroyExcalidrawEditor };
|