4 // --------------------------------------------------
7 console.error('[ace] dbx not found');
11 const dbx = window.dbx;
13 function log(...args) { dbx.log('[ace]', ...args); }
14 function warn(...args) { dbx.warn('[ace]', ...args); }
15 function error(...args) { dbx.error('[ace]', ...args); }
19 dbx.feature.register('ace', {
21 scope: "element", // 🔥 FIX (einzige Änderung)
26 ['css', 'design', 'c-ace.css']
30 ['js', 'lib', 'ajax.js']
36 function init(el, cfg) {
38 log('init START', el, cfg);
40 if (el.__aceInitialized || el.__aceInitializing) {
41 log('already initialized');
45 el.__aceInitializing = true;
47 function resetInitState() {
48 el.__aceInitializing = false;
49 el.__aceInitialized = false;
52 loadAce(function (ok) {
54 log('loadAce callback');
56 if (!ok || !window.ace || typeof window.ace.edit !== 'function') {
58 error('Ace ist nicht geladen.');
62 if (!el || !el.isConnected) {
64 warn('init skipped: element is no longer connected');
68 const file = cfg.file || '';
70 const container = el.closest('.c-ace');
71 log('container (.c-ace):', container);
73 const textarea = container
74 ? container.querySelector('textarea')
79 error('textarea not found in container');
83 textarea.style.display = 'none';
85 if (cfg.height) el.style.height = cfg.height;
86 if (cfg.width) el.style.width = cfg.width;
88 log('element size BEFORE init', {
89 offsetHeight: el.offsetHeight,
90 offsetWidth: el.offsetWidth,
91 clientHeight: el.clientHeight,
92 styleHeight: el.style.height
97 const editor = ace.edit(el);
98 el.__aceEditor = editor;
99 el.__aceInitialized = true;
100 el.__aceInitializing = false;
102 log('editor created', editor);
104 // --------------------------------------------------
106 // --------------------------------------------------
108 function resolveTheme(name) {
109 if (!name) return 'monokai';
110 const t = name.toLowerCase();
111 if (t === 'dark') return 'monokai';
112 if (t === 'light') return 'github';
117 editor.setTheme("ace/theme/" + resolveTheme(cfg.theme));
119 editor.setTheme("ace/theme/monokai");
122 editor.session.setMode(getMode(file));
124 const dirtyEl = container ? container.querySelector('.editor-dirty') : null;
126 function setDirty(state) {
128 log('setDirty:', state, dirtyEl);
131 dirtyEl.dataset.state = state ? 'dirty' : '';
133 warn('dirtyEl not found');
137 // --------------------------------------------------
139 // --------------------------------------------------
141 editor.setValue(textarea.value || '', -1);
144 log('AFTER setValue size', {
145 offsetHeight: el.offsetHeight,
146 clientHeight: el.clientHeight
149 // --------------------------------------------------
150 // 🔥 FIX: LIVE RESIZE (OHNE DELAY)
151 // --------------------------------------------------
153 function doResize() {
161 window.addEventListener('resize', doResize);
163 // 🔥 wichtig: window resize (drag/resize von openWin)
164 const win = el.closest('.dbx-window');
166 if (win && window.ResizeObserver) {
170 const ro = new ResizeObserver(() => {
171 cancelAnimationFrame(raf);
172 raf = requestAnimationFrame(() => editor.resize());
178 // fallback (falls ResizeObserver fehlt)
185 // --------------------------------------------------
187 // --------------------------------------------------
189 window.__dbxEditors = window.__dbxEditors || [];
199 window.__dbxEditors.push(entry);
201 editor.on('focus', () => window.__dbxActiveEditor = entry);
202 el.addEventListener('mousedown', () => window.__dbxActiveEditor = entry);
204 // --------------------------------------------------
206 // --------------------------------------------------
208 editor.session.on('change', function () {
210 const val = editor.getValue();
211 textarea.value = val;
218 addSaveButton(editor, file, entry, setDirty);
222 error('editor init failed', e);
228 // --------------------------------------------------
230 // --------------------------------------------------
232 function loadAce(callback) {
234 const libPath = dbx.config.libPath || '';
235 const root = libPath.replace(/js\/lib\/?$/, '');
236 const acePath = root + 'add_ons/ace/';
238 function configureAcePaths() {
239 if (window.ace && ace.config) {
240 ace.config.set("basePath", acePath);
241 ace.config.set("modePath", acePath);
242 ace.config.set("themePath", acePath);
243 ace.config.set("workerPath", acePath);
247 if (window.ace && typeof window.ace.edit === 'function') {
249 return callback(true);
252 if (window.__dbxAceQueue) {
253 window.__dbxAceQueue.push(callback);
257 window.__dbxAceQueue = [callback];
259 const script = document.createElement('script');
261 script.src = acePath + 'ace.js';
263 script.onload = function () {
267 const q = window.__dbxAceQueue;
268 window.__dbxAceQueue = null;
269 const ok = !!(window.ace && typeof window.ace.edit === 'function');
270 q.forEach(fn => fn(ok));
273 script.onerror = function () {
274 error('FAILED:', script.src);
275 const q = window.__dbxAceQueue || [];
276 window.__dbxAceQueue = null;
277 q.forEach(fn => fn(false));
280 document.head.appendChild(script);
283 // --------------------------------------------------
285 // --------------------------------------------------
287 function getMode(file) {
289 if (!file) return "ace/mode/text";
291 const ext = file.split('.').pop().toLowerCase();
293 if (ext === 'css') return "ace/mode/css";
294 if (ext === 'htm' || ext === 'html') return "ace/mode/html";
295 if (ext === 'js') return "ace/mode/javascript";
296 if (ext === 'php') return "ace/mode/php";
298 return "ace/mode/text";
301 // --------------------------------------------------
302 // SAVE BUTTON (unverändert)
303 // --------------------------------------------------
305 function addSaveButton(editor, file, entry, setDirty) {
307 const container = editor.container.closest('.c-ace');
308 log('addSaveButton container:', container);
311 warn('no editor container found');
315 const btnSave = container.querySelector('.editor-save');
316 const btnDelete = container.querySelector('.editor-delete');
317 const btnRename = container.querySelector('.editor-rename');
318 const btnCopy = container.querySelector('.editor-copy');
319 const input = container.querySelector('.editor-filename');
320 const security = container.querySelector('.dbx-editor-security');
323 warn('no save button found');
327 function showMsg(txt, type='ok') {
329 const el = document.createElement('div');
330 el.className = 'dbx-msg';
331 el.textContent = txt;
333 el.style.position = 'fixed';
334 el.style.top = '20px';
335 el.style.right = '20px';
336 el.style.zIndex = 999999;
337 el.style.padding = '6px 10px';
338 el.style.borderRadius = '4px';
339 el.style.background = (type === 'error') ? '#dc3545' : '#198754';
340 el.style.color = '#fff';
341 el.style.fontSize = '12px';
342 el.style.boxShadow = '0 2px 6px rgba(0,0,0,0.2)';
343 el.style.opacity = '0';
345 document.body.appendChild(el);
347 requestAnimationFrame(() => el.style.opacity = '1');
350 el.style.opacity = '0';
351 setTimeout(() => el.remove(), 300);
355 if (input) input.value = file;
357 const confirmDeleteText = (entry?.cfg?.confirm_delete ?? 'Datei wirklich löschen?');
358 const confirmRenameText = (entry?.cfg?.confirm_rename ?? 'Datei wirklich umbenennen?');
359 const confirmCopyText = (entry?.cfg?.confirm_copy ?? 'Datei wirklich kopieren?');
361 function doConfirm(text, msg) {
362 if (text === '-') return true;
363 return confirm(text + '\n' + msg);
366 function setIcon(state) {
368 const icon = btnSave.querySelector('i');
371 icon.className = 'bi';
373 if (state === 'saving') icon.classList.add('bi-arrow-repeat');
374 else if (state === 'saved') icon.classList.add('bi-check');
375 else if (state === 'error') icon.classList.add('bi-x');
376 else icon.classList.add('bi-floppy');
381 function requestJson(url, options = {}) {
382 if (!dbx.ajax || typeof dbx.ajax.request !== 'function') {
383 return Promise.reject(new Error('ajax.js nicht geladen.'));
385 return dbx.ajax.request(Object.assign({
394 * Sendet eine Dateimutation ausschließlich per POST und ergänzt den
395 * aktuellen dbxForm-Token. Jede Antwort rotiert den Token, damit auch
396 * mehrere Speichern-/Kopieren-Aktionen in einem Editorfenster möglich
397 * bleiben, ohne einen bereits verbrauchten Token wiederzuverwenden.
399 function requestMutation(data) {
400 if (!security || !security.name || !security.value) {
401 return Promise.reject(new Error('dbxForm-Sicherheitstoken fehlt.'));
404 const action = String(data?.action || '');
405 const body = new URLSearchParams();
406 Object.entries(data || {}).forEach(([name, value]) => {
407 if (name === 'action') return;
408 body.set(name, String(value ?? ''));
410 body.set(security.name, security.value);
412 return requestJson('?dbx_modul=dbxEditor&dbx_run1=' + encodeURIComponent(action), {
414 headers: {'Content-Type': 'application/x-www-form-urlencoded'},
415 body: body.toString()
417 if (res?.security?.name && res?.security?.value) {
418 security.name = res.security.name;
419 security.value = res.security.value;
427 if (btnSave.dataset.busy === '1') return;
428 btnSave.dataset.busy = '1';
430 const content = editor.getValue();
431 const currentFile = input ? input.value : file;
433 btnSave.dataset.state = 'saving';
445 btnSave.dataset.state = 'saved';
449 showMsg('Datei gespeichert');
452 btnSave.dataset.state = '';
457 btnSave.dataset.state = 'error';
459 showMsg('Speichern fehlgeschlagen', 'error');
462 btnSave.dataset.busy = '0';
465 btnSave.dataset.state = 'error';
467 showMsg('Speichern fehlgeschlagen', 'error');
468 btnSave.dataset.busy = '0';
472 btnSave.onclick = doSave;
477 btnDelete.onclick = function () {
479 const currentFile = input ? input.value : file;
481 if (!doConfirm(confirmDeleteText, currentFile)) return;
491 showMsg('Datei gelöscht');
493 const win = container.closest('.dbx-window');
494 if (win) win.remove();
497 showMsg('Löschen fehlgeschlagen', 'error');
505 if (btnRename && input) {
507 btnRename.onclick = function () {
509 const newFile = input.value.trim();
511 if (!newFile || newFile === oldValue) {
512 showMsg('Dateiname unverändert');
516 if (!doConfirm(confirmRenameText, oldValue + ' → ' + newFile)) return;
528 showMsg('Datei umbenannt');
531 showMsg('Umbenennen fehlgeschlagen', 'error');
532 input.value = oldValue;
538 if (btnCopy && input) {
540 btnCopy.onclick = function () {
542 const newFile = input.value.trim();
544 if (!newFile || newFile === oldValue) {
545 showMsg('Dateiname unverändert');
549 if (!doConfirm(confirmCopyText, oldValue + ' → ' + newFile)) return;
560 showMsg('Datei kopiert');
563 showMsg('Kopieren fehlgeschlagen', 'error');
569 log('save/delete/rename/copy wired');
572 if (!window.__dbxSaveHandlerInstalled) {
574 window.__dbxSaveHandlerInstalled = true;
576 document.addEventListener('keydown', function (e) {
578 const isSave = (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's';
583 if (window.__dbxActiveEditor?.save) {
584 window.__dbxActiveEditor.save();
588 if (window.__dbxEditors?.length) {
589 window.__dbxEditors[0].save();