ObsiGate/frontend/js/utils.js
Bruno Charest 6a55dfd5eb
All checks were successful
CI / lint (push) Successful in 13s
CI / security (push) Successful in 8s
CI / test (push) Successful in 24s
CI / build (push) Successful in 3s
fix: over-aggressive state.xxx replacements on local variables
2026-05-28 17:02:27 -04:00

511 lines
13 KiB
JavaScript

import { state } from './state.js';
// ---------------------------------------------------------------------------
// File extension → Lucide icon mapping
// ---------------------------------------------------------------------------
const EXT_ICONS = {
// Text files
".md": "file-text",
".txt": "file-text",
".log": "file-text",
".readme": "file-text",
".rst": "file-text",
".adoc": "file-text",
// Web development
".html": "file-code",
".htm": "file-code",
".css": "file-code",
".scss": "file-code",
".sass": "file-code",
".less": "file-code",
".js": "file-code",
".jsx": "file-code",
".ts": "file-code",
".tsx": "file-code",
".vue": "file-code",
".svelte": "file-code",
// Programming languages
".py": "file-code",
".java": "file-code",
".c": "file-code",
".cpp": "file-code",
".cc": "file-code",
".cxx": "file-code",
".h": "file-code",
".hpp": "file-code",
".cs": "file-code",
".go": "file-code",
".rs": "file-code",
".rb": "file-code",
".php": "file-code",
".swift": "file-code",
".kt": "file-code",
".scala": "file-code",
".r": "file-code",
".m": "file-code",
".pl": "file-code",
".lua": "file-code",
".dart": "file-code",
".nim": "file-code",
".zig": "file-code",
".odin": "file-code",
".v": "file-code",
".cr": "file-code",
".ex": "file-code",
".exs": "file-code",
".elm": "file-code",
".purs": "file-code",
".hs": "file-code",
".ml": "file-code",
".ocaml": "file-code",
".fs": "file-code",
".fsx": "file-code",
".vb": "file-code",
".pas": "file-code",
".pp": "file-code",
".inc": "file-code",
// Data formats
".json": "file-json",
".yaml": "file-cog",
".yml": "file-cog",
".toml": "file-cog",
".xml": "file-code",
".csv": "table",
".tsv": "table",
".sql": "database",
".db": "database",
".sqlite": "database",
".sqlite3": "database",
".parquet": "database",
".avro": "database",
// Configuration files
".ini": "file-cog",
".cfg": "file-cog",
".conf": "file-cog",
".env": "file-cog",
".dockerfile": "file-cog",
".gitignore": "file-cog",
".gitattributes": "file-cog",
".editorconfig": "file-cog",
".eslintrc": "file-cog",
".prettierrc": "file-cog",
".babelrc": "file-cog",
".tsconfig": "file-cog",
"package.json": "file-cog",
"package-lock.json": "file-cog",
"yarn.lock": "file-cog",
"composer.json": "file-cog",
"requirements.txt": "file-cog",
"pipfile": "file-cog",
"gemfile": "file-cog",
"cargo.toml": "file-cog",
"go.mod": "file-cog",
"go.sum": "file-cog",
"pom.xml": "file-cog",
"build.gradle": "file-cog",
"cmakelists.txt": "file-cog",
"makefile": "file-cog",
// Shell scripts
".sh": "terminal",
".bash": "terminal",
".zsh": "terminal",
".fish": "terminal",
".bat": "terminal",
".cmd": "terminal",
".ps1": "terminal",
".psm1": "terminal",
".psd1": "terminal",
// Document formats
".pdf": "file-text",
".doc": "file-text",
".docx": "file-text",
".rtf": "file-text",
".odt": "file-text",
".tex": "file-text",
".latex": "file-text",
// Image files
".png": "file-image",
".jpg": "file-image",
".jpeg": "file-image",
".gif": "file-image",
".svg": "file-image",
".webp": "file-image",
".bmp": "file-image",
".ico": "file-image",
".tiff": "file-image",
".tif": "file-image",
// Audio files
".mp3": "file-music",
".wav": "file-music",
".flac": "file-music",
".aac": "file-music",
".ogg": "file-music",
".m4a": "file-music",
".wma": "file-music",
// Video files
".mp4": "play",
".avi": "play",
".mov": "play",
".wmv": "play",
".flv": "play",
".webm": "play",
".mkv": "play",
".m4v": "play",
".3gp": "play",
// Archive files
".zip": "file-archive",
".rar": "file-archive",
".7z": "file-archive",
".tar": "file-archive",
".gz": "file-archive",
".tgz": "file-archive",
".bz2": "file-archive",
".xz": "file-archive",
".deb": "file-archive",
".rpm": "file-archive",
".dmg": "file-archive",
".pkg": "file-archive",
".msi": "file-archive",
".exe": "file-archive",
// Font files
".ttf": "file-type",
".otf": "file-type",
".woff": "file-type",
".woff2": "file-type",
".eot": "file-type",
// Other common files
".key": "file-cog",
".pem": "file-cog",
".crt": "file-cog",
".cert": "file-cog",
".p12": "file-cog",
".pfx": "file-cog",
".lock": "file-cog",
".tmp": "file",
".bak": "file",
".old": "file",
".orig": "file",
".save": "file",
};
function getFileIcon(name) {
const ext = "." + name.split(".").pop().toLowerCase();
return EXT_ICONS[ext] || "file";
}
// ---------------------------------------------------------------------------
// Safe CDN helpers
// ---------------------------------------------------------------------------
let _iconDebounceTimer = null;
/**
* Debounced icon creation — batches multiple rapid calls into one
* DOM scan to avoid excessive reflows when building large trees.
*/
function safeCreateIcons() {
if (typeof lucide === "undefined" || !lucide.createIcons) return;
if (state._iconDebounceTimer) return; // already scheduled
state._iconDebounceTimer = requestAnimationFrame(() => {
state._iconDebounceTimer = null;
try {
lucide.createIcons();
} catch (e) {
/* CDN not loaded */
}
});
}
/** Force-flush icon creation immediately (use sparingly). */
function flushIcons() {
if (state._iconDebounceTimer) {
cancelAnimationFrame(state._iconDebounceTimer);
state._iconDebounceTimer = null;
}
if (typeof lucide !== "undefined" && lucide.createIcons) {
try {
lucide.createIcons();
} catch (e) {
/* CDN not loaded */
}
}
}
function safeHighlight(block) {
if (typeof hljs !== "undefined" && hljs.highlightElement) {
try {
hljs.highlightElement(block);
} catch (e) {
/* CDN not loaded */
}
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function escapeHtml(str) {
if (!str) return "";
return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
// ---------------------------------------------------------------------------
// Editor (CodeMirror)
// ---------------------------------------------------------------------------
async function openEditor(vaultName, filePath) {
state.editorVault = vaultName;
state.editorPath = filePath;
const modal = document.getElementById("editor-modal");
const titleEl = document.getElementById("editor-title");
const bodyEl = document.getElementById("editor-body");
titleEl.textContent = `Édition: ${filePath.split("/").pop()}`;
// Fetch raw content
const rawUrl = `/api/file/${encodeURIComponent(vaultName)}/raw?path=${encodeURIComponent(filePath)}`;
const rawData = await api(rawUrl);
// Clear previous editor
bodyEl.innerHTML = "";
if (state.editorView) {
state.editorView.destroy();
state.editorView = null;
}
state.fallbackEditorEl = null;
try {
await waitForCodeMirror();
const { EditorView, EditorState, basicSetup, markdown, python, javascript, html, css, json, xml, sql, php, cpp, java, rust, oneDark, keymap } = window.CodeMirror;
const currentTheme = document.documentElement.getAttribute("data-theme");
const fileExt = filePath.split(".").pop().toLowerCase();
const extensions = [
basicSetup,
keymap.of([
{
key: "Mod-s",
run: () => {
saveFile();
return true;
},
},
]),
EditorView.lineWrapping,
];
// Add language support based on file extension
const langMap = {
md: markdown,
markdown: markdown,
py: python,
js: javascript,
jsx: javascript,
ts: javascript,
tsx: javascript,
mjs: javascript,
cjs: javascript,
html: html,
htm: html,
css: css,
scss: css,
less: css,
json: json,
xml: xml,
svg: xml,
sql: sql,
php: php,
cpp: cpp,
cc: cpp,
cxx: cpp,
c: cpp,
h: cpp,
hpp: cpp,
java: java,
rs: rust,
sh: javascript, // Using javascript for shell scripts as fallback
bash: javascript,
zsh: javascript,
};
const langMode = langMap[fileExt];
if (langMode) {
extensions.push(langMode());
}
if (currentTheme === "dark") {
extensions.push(oneDark);
}
const state = EditorState.create({
doc: rawData.raw,
extensions: extensions,
});
state.editorView = new EditorView({
state: state,
parent: bodyEl,
});
} catch (err) {
console.error("CodeMirror init failed, falling back to textarea:", err);
state.fallbackEditorEl = document.createElement("textarea");
state.fallbackEditorEl.className = "fallback-editor";
state.fallbackEditorEl.value = rawData.raw;
bodyEl.appendChild(state.fallbackEditorEl);
}
modal.classList.add("active");
safeCreateIcons();
}
async function waitForCodeMirror() {
let attempts = 0;
while (!window.CodeMirror && attempts < 50) {
await new Promise((resolve) => setTimeout(resolve, 100));
attempts++;
}
if (!window.CodeMirror) {
throw new Error("CodeMirror failed to load");
}
}
function closeEditor() {
const modal = document.getElementById("editor-modal");
modal.classList.remove("active");
if (state.editorView) {
state.editorView.destroy();
state.editorView = null;
}
state.fallbackEditorEl = null;
state.editorVault = null;
state.editorPath = null;
}
async function saveFile() {
if ((!editorView && !state.fallbackEditorEl) || !editorVault || !state.editorPath) return;
const content = editorView ? state.editorView.state.doc.toString() : state.fallbackEditorEl.value;
const saveBtn = document.getElementById("editor-save");
const originalHTML = saveBtn.innerHTML;
try {
saveBtn.disabled = true;
saveBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
safeCreateIcons();
const response = await fetch(`/api/file/${encodeURIComponent(state.editorVault)}/save?path=${encodeURIComponent(state.editorPath)}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Erreur de sauvegarde");
}
saveBtn.innerHTML = '<i data-lucide="check" style="width:16px;height:16px"></i>';
safeCreateIcons();
setTimeout(() => {
closeEditor();
if (state.currentVault === editorVault && state.currentPath === state.editorPath) {
openFile(state.currentVault, state.currentPath);
}
}, 800);
} catch (err) {
console.error("Save error:", err);
alert(`Erreur: ${err.message}`);
saveBtn.innerHTML = originalHTML;
saveBtn.disabled = false;
safeCreateIcons();
}
}
async function deleteFile() {
if (!editorVault || !state.editorPath) return;
const deleteBtn = document.getElementById("editor-delete");
const originalHTML = deleteBtn.innerHTML;
try {
deleteBtn.disabled = true;
deleteBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
safeCreateIcons();
const response = await fetch(`/api/file/${encodeURIComponent(state.editorVault)}?path=${encodeURIComponent(state.editorPath)}`, { method: "DELETE" });
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Erreur de suppression");
}
closeEditor();
showWelcome();
await refreshSidebarForContext();
await refreshTagsForContext();
} catch (err) {
console.error("Delete error:", err);
alert(`Erreur: ${err.message}`);
deleteBtn.innerHTML = originalHTML;
deleteBtn.disabled = false;
safeCreateIcons();
}
}
function initEditor() {
const cancelBtn = document.getElementById("editor-cancel");
const deleteBtn = document.getElementById("editor-delete");
const saveBtn = document.getElementById("editor-save");
const modal = document.getElementById("editor-modal");
cancelBtn.addEventListener("click", closeEditor);
deleteBtn.addEventListener("click", deleteFile);
saveBtn.addEventListener("click", saveFile);
// Close on overlay click
modal.addEventListener("click", (e) => {
if (e.target === modal) {
closeEditor();
}
});
// ESC to close
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
closeEditor();
}
});
// Fix mouse wheel scrolling in editor
modal.addEventListener(
"wheel",
(e) => {
const editorBody = document.getElementById("editor-body");
if (editorBody && editorBody.contains(e.target)) {
// Let the editor handle the scroll
return;
}
// Prevent modal from scrolling if not in editor area
e.preventDefault();
},
{ passive: false },
);
}
export { getFileIcon, safeCreateIcons, flushIcons, safeHighlight, escapeHtml, EXT_ICONS, openEditor, closeEditor, saveFile, deleteFile, waitForCodeMirror, initEditor };