dbxapp 4.1.3
CMS, Shop, Workflows und modulare Geschäftsanwendungen
Loading...
Searching...
No Matches
ace.js
Go to the documentation of this file.
1(function () {
2
3
4 // --------------------------------------------------
5
6 if (!window.dbx) {
7 console.error('[ace] dbx not found');
8 return;
9 }
10
11 const dbx = window.dbx;
12
13 function log(...args) { dbx.log('[ace]', ...args); }
14 function warn(...args) { dbx.warn('[ace]', ...args); }
15 function error(...args) { dbx.error('[ace]', ...args); }
16
17 log('lib loaded');
18
19 dbx.feature.register('ace', {
20
21 scope: "element", // 🔥 FIX (einzige Änderung)
22
23 prio: 'last',
24
25 css: [
26 ['css', 'design', 'c-ace.css']
27 ],
28
29 js: [
30 ['js', 'lib', 'ajax.js']
31 ],
32
33 init: init
34 });
35
36 function init(el, cfg) {
37
38 log('init START', el, cfg);
39
40 if (el.__aceInitialized || el.__aceInitializing) {
41 log('already initialized');
42 return;
43 }
44
45 el.__aceInitializing = true;
46
47 function resetInitState() {
48 el.__aceInitializing = false;
49 el.__aceInitialized = false;
50 }
51
52 loadAce(function (ok) {
53
54 log('loadAce callback');
55
56 if (!ok || !window.ace || typeof window.ace.edit !== 'function') {
57 resetInitState();
58 error('Ace ist nicht geladen.');
59 return;
60 }
61
62 if (!el || !el.isConnected) {
63 resetInitState();
64 warn('init skipped: element is no longer connected');
65 return;
66 }
67
68 const file = cfg.file || '';
69
70 const container = el.closest('.c-ace');
71 log('container (.c-ace):', container);
72
73 const textarea = container
74 ? container.querySelector('textarea')
75 : null;
76
77 if (!textarea) {
78 resetInitState();
79 error('textarea not found in container');
80 return;
81 }
82
83 textarea.style.display = 'none';
84
85 if (cfg.height) el.style.height = cfg.height;
86 if (cfg.width) el.style.width = cfg.width;
87
88 log('element size BEFORE init', {
89 offsetHeight: el.offsetHeight,
90 offsetWidth: el.offsetWidth,
91 clientHeight: el.clientHeight,
92 styleHeight: el.style.height
93 });
94
95 try {
96
97 const editor = ace.edit(el);
98 el.__aceEditor = editor;
99 el.__aceInitialized = true;
100 el.__aceInitializing = false;
101
102 log('editor created', editor);
103
104 // --------------------------------------------------
105 // THEME
106 // --------------------------------------------------
107
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';
113 return t;
114 }
115
116 try {
117 editor.setTheme("ace/theme/" + resolveTheme(cfg.theme));
118 } catch {
119 editor.setTheme("ace/theme/monokai");
120 }
121
122 editor.session.setMode(getMode(file));
123
124 const dirtyEl = container ? container.querySelector('.editor-dirty') : null;
125
126 function setDirty(state) {
127
128 log('setDirty:', state, dirtyEl);
129
130 if (dirtyEl) {
131 dirtyEl.dataset.state = state ? 'dirty' : '';
132 } else {
133 warn('dirtyEl not found');
134 }
135 }
136
137 // --------------------------------------------------
138 // CONTENT INIT
139 // --------------------------------------------------
140
141 editor.setValue(textarea.value || '', -1);
142 setDirty(false);
143
144 log('AFTER setValue size', {
145 offsetHeight: el.offsetHeight,
146 clientHeight: el.clientHeight
147 });
148
149 // --------------------------------------------------
150 // 🔥 FIX: LIVE RESIZE (OHNE DELAY)
151 // --------------------------------------------------
152
153 function doResize() {
154 editor.resize();
155 }
156
157 // initial
158 doResize();
159
160 // window resize
161 window.addEventListener('resize', doResize);
162
163 // 🔥 wichtig: window resize (drag/resize von openWin)
164 const win = el.closest('.dbx-window');
165
166 if (win && window.ResizeObserver) {
167
168 let raf;
169
170 const ro = new ResizeObserver(() => {
171 cancelAnimationFrame(raf);
172 raf = requestAnimationFrame(() => editor.resize());
173 });
174
175 ro.observe(win);
176 }
177
178 // fallback (falls ResizeObserver fehlt)
179 else {
180 setTimeout(() => {
181 editor.resize();
182 }, 0);
183 }
184
185 // --------------------------------------------------
186 // REGISTRY
187 // --------------------------------------------------
188
189 window.__dbxEditors = window.__dbxEditors || [];
190
191 const entry = {
192 editor,
193 container,
194 textarea,
195 cfg,
196 save: null
197 };
198
199 window.__dbxEditors.push(entry);
200
201 editor.on('focus', () => window.__dbxActiveEditor = entry);
202 el.addEventListener('mousedown', () => window.__dbxActiveEditor = entry);
203
204 // --------------------------------------------------
205 // CHANGE
206 // --------------------------------------------------
207
208 editor.session.on('change', function () {
209
210 const val = editor.getValue();
211 textarea.value = val;
212
213 setDirty(true);
214
215 });
216
217 // SAVE BUTTON
218 addSaveButton(editor, file, entry, setDirty);
219
220 } catch (e) {
221 resetInitState();
222 error('editor init failed', e);
223 }
224
225 });
226 }
227
228 // --------------------------------------------------
229 // ACE LOADER
230 // --------------------------------------------------
231
232 function loadAce(callback) {
233
234 const libPath = dbx.config.libPath || '';
235 const root = libPath.replace(/js\/lib\/?$/, '');
236 const acePath = root + 'add_ons/ace/';
237
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);
244 }
245 }
246
247 if (window.ace && typeof window.ace.edit === 'function') {
248 configureAcePaths();
249 return callback(true);
250 }
251
252 if (window.__dbxAceQueue) {
253 window.__dbxAceQueue.push(callback);
254 return;
255 }
256
257 window.__dbxAceQueue = [callback];
258
259 const script = document.createElement('script');
260
261 script.src = acePath + 'ace.js';
262
263 script.onload = function () {
264
265 configureAcePaths();
266
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));
271 };
272
273 script.onerror = function () {
274 error('FAILED:', script.src);
275 const q = window.__dbxAceQueue || [];
276 window.__dbxAceQueue = null;
277 q.forEach(fn => fn(false));
278 };
279
280 document.head.appendChild(script);
281 }
282
283 // --------------------------------------------------
284 // MODE
285 // --------------------------------------------------
286
287 function getMode(file) {
288
289 if (!file) return "ace/mode/text";
290
291 const ext = file.split('.').pop().toLowerCase();
292
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";
297
298 return "ace/mode/text";
299 }
300
301 // --------------------------------------------------
302 // SAVE BUTTON (unverändert)
303 // --------------------------------------------------
304
305 function addSaveButton(editor, file, entry, setDirty) {
306
307 const container = editor.container.closest('.c-ace');
308 log('addSaveButton container:', container);
309
310 if (!container) {
311 warn('no editor container found');
312 return;
313 }
314
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');
321
322 if (!btnSave) {
323 warn('no save button found');
324 return;
325 }
326
327 function showMsg(txt, type='ok') {
328
329 const el = document.createElement('div');
330 el.className = 'dbx-msg';
331 el.textContent = txt;
332
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';
344
345 document.body.appendChild(el);
346
347 requestAnimationFrame(() => el.style.opacity = '1');
348
349 setTimeout(() => {
350 el.style.opacity = '0';
351 setTimeout(() => el.remove(), 300);
352 }, 1200);
353 }
354
355 if (input) input.value = file;
356
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?');
360
361 function doConfirm(text, msg) {
362 if (text === '-') return true;
363 return confirm(text + '\n' + msg);
364 }
365
366 function setIcon(state) {
367
368 const icon = btnSave.querySelector('i');
369 if (!icon) return;
370
371 icon.className = 'bi';
372
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');
377 }
378
379 setIcon();
380
381 function requestJson(url, options = {}) {
382 if (!dbx.ajax || typeof dbx.ajax.request !== 'function') {
383 return Promise.reject(new Error('ajax.js nicht geladen.'));
384 }
385 return dbx.ajax.request(Object.assign({
386 url: url,
387 method: 'GET',
388 mode: 'json',
389 timeout: 30000
390 }, options));
391 }
392
393 /**
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.
398 */
399 function requestMutation(data) {
400 if (!security || !security.name || !security.value) {
401 return Promise.reject(new Error('dbxForm-Sicherheitstoken fehlt.'));
402 }
403
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 ?? ''));
409 });
410 body.set(security.name, security.value);
411
412 return requestJson('?dbx_modul=dbxEditor&dbx_run1=' + encodeURIComponent(action), {
413 method: 'POST',
414 headers: {'Content-Type': 'application/x-www-form-urlencoded'},
415 body: body.toString()
416 }).then(res => {
417 if (res?.security?.name && res?.security?.value) {
418 security.name = res.security.name;
419 security.value = res.security.value;
420 }
421 return res;
422 });
423 }
424
425 function doSave() {
426
427 if (btnSave.dataset.busy === '1') return;
428 btnSave.dataset.busy = '1';
429
430 const content = editor.getValue();
431 const currentFile = input ? input.value : file;
432
433 btnSave.dataset.state = 'saving';
434 setIcon('saving');
435
436 requestMutation({
437 action: 'save',
438 file: currentFile,
439 content: content
440 })
441 .then(res => {
442
443 if (res.ok) {
444
445 btnSave.dataset.state = 'saved';
446 setIcon('saved');
447 setDirty(false);
448
449 showMsg('Datei gespeichert');
450
451 setTimeout(() => {
452 btnSave.dataset.state = '';
453 setIcon();
454 }, 1200);
455
456 } else {
457 btnSave.dataset.state = 'error';
458 setIcon('error');
459 showMsg('Speichern fehlgeschlagen', 'error');
460 }
461
462 btnSave.dataset.busy = '0';
463 })
464 .catch(() => {
465 btnSave.dataset.state = 'error';
466 setIcon('error');
467 showMsg('Speichern fehlgeschlagen', 'error');
468 btnSave.dataset.busy = '0';
469 });
470 }
471
472 btnSave.onclick = doSave;
473 entry.save = doSave;
474
475 if (btnDelete) {
476
477 btnDelete.onclick = function () {
478
479 const currentFile = input ? input.value : file;
480
481 if (!doConfirm(confirmDeleteText, currentFile)) return;
482
483 requestMutation({
484 action: 'delete',
485 file: currentFile
486 })
487 .then(res => {
488
489 if (res.ok) {
490
491 showMsg('Datei gelöscht');
492
493 const win = container.closest('.dbx-window');
494 if (win) win.remove();
495
496 } else {
497 showMsg('Löschen fehlgeschlagen', 'error');
498 }
499 });
500 };
501 }
502
503 let oldValue = file;
504
505 if (btnRename && input) {
506
507 btnRename.onclick = function () {
508
509 const newFile = input.value.trim();
510
511 if (!newFile || newFile === oldValue) {
512 showMsg('Dateiname unverändert');
513 return;
514 }
515
516 if (!doConfirm(confirmRenameText, oldValue + ' → ' + newFile)) return;
517
518 requestMutation({
519 action: 'rename',
520 old: oldValue,
521 new: newFile
522 })
523 .then(res => {
524
525 if (res.ok) {
526
527 oldValue = newFile;
528 showMsg('Datei umbenannt');
529
530 } else {
531 showMsg('Umbenennen fehlgeschlagen', 'error');
532 input.value = oldValue;
533 }
534 });
535 };
536 }
537
538 if (btnCopy && input) {
539
540 btnCopy.onclick = function () {
541
542 const newFile = input.value.trim();
543
544 if (!newFile || newFile === oldValue) {
545 showMsg('Dateiname unverändert');
546 return;
547 }
548
549 if (!doConfirm(confirmCopyText, oldValue + ' → ' + newFile)) return;
550
551 requestMutation({
552 action: 'copy',
553 old: oldValue,
554 new: newFile
555 })
556 .then(res => {
557
558 if (res.ok) {
559
560 showMsg('Datei kopiert');
561
562 } else {
563 showMsg('Kopieren fehlgeschlagen', 'error');
564 }
565 });
566 };
567 }
568
569 log('save/delete/rename/copy wired');
570 }
571
572 if (!window.__dbxSaveHandlerInstalled) {
573
574 window.__dbxSaveHandlerInstalled = true;
575
576 document.addEventListener('keydown', function (e) {
577
578 const isSave = (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's';
579 if (!isSave) return;
580
581 e.preventDefault();
582
583 if (window.__dbxActiveEditor?.save) {
584 window.__dbxActiveEditor.save();
585 return;
586 }
587
588 if (window.__dbxEditors?.length) {
589 window.__dbxEditors[0].save();
590 }
591 });
592 }
593
594})();