dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
icons_editor.js
Go to the documentation of this file.
1(function () {
2
3 if (!window.dbx) return;
4 const dbx = window.dbx;
5
6 function log(...a) { dbx.log('[icons_editor]', 'β†’', ...a); }
7 function warn(...a) { dbx.warn('[icons_editor]', 'β†’', ...a); }
8
9 const ICON_CLASS = 'dbx-editor-icon';
10 const ICON_Z_INDEX_MIN = 2400;
11 const ICON_Z_INDEX_OFFSET = 240;
12 const ICON_STACK_STEP_X = 32;
13 const ICON_STACK_STEP_Y = 28;
14 const ICON_COLLISION_WIDTH = 32;
15 const ICON_COLLISION_HEIGHT = 26;
16
17 dbx.feature.register("icons_editor", {
18
19 scope: "global", // πŸ”₯ FIX
20 priority: "last",
21
22 // πŸ”₯ NEU: CSS deklarativ (PREPARE ΓΌbernimmt)
23 css: [
24 ['css', 'design', 'c-icons_editor.css']
25 ],
26
27 init(el, cfg) {
28
29 log("init start", cfg);
30
31 const root = document.body;
32 const editorMenuFiles = window.__dbxEditorMenuFiles || new Map();
33 window.__dbxEditorMenuFiles = editorMenuFiles;
34
35 collectEditorFiles();
36 renderEditorFilesMenu();
37 watchEditorFilesData();
38
39 // --------------------------------------------------
40 // INIT GUARD (auf Core-Standard gebracht)
41 // --------------------------------------------------
42
43 el.__dbxInitialized = el.__dbxInitialized || {};
44 if (el.__dbxInitialized["icons_editor"]) {
45 log("already initialized on element, skip");
46 return;
47 }
48 el.__dbxInitialized["icons_editor"] = true;
49
50 // --------------------------------------------------
51 // DEPENDENCY (kein dbx.load mehr!)
52 // --------------------------------------------------
53
54 log("resolve deps: openWin");
55
56 dbx.resolveFeature("openWin", function () {
57
58 log("deps ready");
59
60 const items = [];
61
62 // --------------------------------------------------
63 // SCAN
64 // --------------------------------------------------
65
66 dbx.iconsEditor = dbx.iconsEditor || {};
67 dbx.iconsEditor.rescan = function (scanRoot) {
68 runWhenRendered(() => scanEditorMarkers(scanRoot));
69 };
70 this.rescan = dbx.iconsEditor.rescan;
71
72 runWhenRendered(() => scanEditorMarkers(root));
73
74 function runWhenRendered(callback) {
75 const run = () => {
76 requestAnimationFrame(() => {
77 requestAnimationFrame(() => {
78 setTimeout(callback, 0);
79 });
80 });
81 };
82
83 if (document.readyState === 'complete') {
84 run();
85 return;
86 }
87
88 window.addEventListener('load', run, { once: true });
89 }
90
91 function scanEditorMarkers(scanRoot) {
92 const scope = scanRoot || document.body;
93
94 collectEditorFiles();
95 renderEditorFilesMenu();
96
97 log("scanning DOM (scope)", scope);
98
99 const comments = [];
100
101 const walker = document.createTreeWalker(
102 scope,
103 NodeFilter.SHOW_COMMENT,
104 null,
105 false
106 );
107
108 let node;
109
110 while ((node = walker.nextNode())) {
111 if (node.__dbxEditorIconCreated) {
112 continue;
113 }
114
115 const txt = node.nodeValue.trim();
116 const marker = parseMarker(txt);
117
118 if (marker) {
119 comments.push({ node, marker });
120 }
121 }
122
123 log("found comments:", comments.length);
124
125 // --------------------------------------------------
126 // CREATE ICONS
127 // --------------------------------------------------
128
129 const seenTplPaths = new Set();
130 const seenTplTableCols = new Set();
131
132 comments.forEach(item => {
133
134 const node = item.node;
135 const meta = item.marker;
136 const path = meta.path || '';
137
138 // Nur Template-HTML-Editor (πŸ–οΈ HTML bearbeiten).
139 if (meta.kind !== 'tpl') {
140 node.__dbxEditorIconCreated = true;
141 return;
142 }
143
144 const inTableCell = isTableCellMarker(node);
145
146 if (path && inTableCell) {
147 const cell = node.parentElement;
148 const colKey = path + '|' + (cell ? cell.cellIndex : 0);
149
150 // Pro Tabellenspalte ein Stift (erste Zeile im DOM).
151 if (seenTplTableCols.has(colKey)) {
152 node.__dbxEditorIconCreated = true;
153 return;
154 }
155 seenTplTableCols.add(colKey);
156 } else if (path) {
157 if (seenTplPaths.has(path)) {
158 node.__dbxEditorIconCreated = true;
159 return;
160 }
161 seenTplPaths.add(path);
162 }
163
164 const target = normalizeTarget(findTargetElement(node));
165
166 if (!target) {
167 warn("no target found for", path);
168 return;
169 }
170
171 createIcon(target, meta);
172 node.__dbxEditorIconCreated = true;
173 });
174 }
175
176 // --------------------------------------------------
177 // EVENTS (GLOBAL)
178 // --------------------------------------------------
179
180 if (!window.__dbxEditorEvents) {
181
182 log("attach global events");
183
184 window.addEventListener('scroll', updateAll);
185 window.addEventListener('resize', updateAll);
186 window.addEventListener('load', updateAll);
187
188 window.__dbxEditorEvents = true;
189 }
190
191 log("initialized, total icons:", items.length);
192
193 // ==================================================
194 // FUNCTIONS
195 // ==================================================
196
197 function createIcon(target, meta) {
198
199 const url = getEditorFileUrl(meta);
200
201 const icon = document.createElement('a');
202
203 icon.href = url;
204 const title = getTemplateOverlayTitle(meta.path || '');
205
206 icon.innerHTML = '<span title="' + escapeHtml(title) + '">πŸ–οΈ</span>';
207 icon.className = ICON_CLASS;
208 icon.title = title;
209
210 icon.setAttribute(
211 'data-dbx',
212 'lib=openWin' +
213 '|url=' + url +
214 '|title=' + encodeDataValue(title) +
215 '|reload=1' +
216 '|width=80%' +
217 '|height=80%' +
218 '|left=center' +
219 '|top=center' +
220 '|prio=last'
221 );
222
223 // EVENTS
224 icon.addEventListener('mouseenter', () => highlightTarget(target, true));
225 icon.addEventListener('mouseleave', () => highlightTarget(target, false));
226
227 icon.style.cursor = 'pointer';
228 icon.style.textDecoration = 'none';
229
230 if (isTableCellTarget(target)) {
231 attachTableCellIcon(icon, target);
232 dbx.rescan(icon);
233 items.push({ icon, target, meta });
234 watchTargetLayout(icon, target, getTemplateNodes(target)[0]);
235 return icon;
236 }
237
238 // DOM INSERT (zuerst!)
239 const anchorNode = getTargetAnchorNode(target);
240 const el = anchorNode || document.body;
241 const container = el.closest('.dbx-window, .dbx-openwin, .dbx-modal, #dbxHeader') || document.body;
242
243 // STYLE
244 icon.style.position = 'absolute';
245 icon.style.zIndex = getEditorIconZIndex(container);
246
247 // DOM INSERT
248 container.appendChild(icon);
249
250 dbx.rescan(icon);
251
252 // πŸ”₯πŸ”₯ FINAL FIX: echten Scroll-Container sauber bestimmen
253 let scrollEl = el;
254 while (scrollEl && scrollEl !== document.body) {
255 const style = window.getComputedStyle(scrollEl);
256
257 if (
258 style.overflowY === 'auto' ||
259 style.overflowY === 'scroll'
260 ) {
261 break;
262 }
263
264 scrollEl = scrollEl.parentElement;
265 }
266
267 // πŸ‘‰ FALL 1: app-layout (container scroll)
268 if (scrollEl && scrollEl !== document.body) {
269
270 if (!scrollEl.__dbxScrollBound) {
271 scrollEl.addEventListener('scroll', updateAll);
272 scrollEl.__dbxScrollBound = true;
273 }
274
275 } else {
276
277 // πŸ‘‰ FALL 2: web-layout (window scroll)
278 if (!window.__dbxEditorEvents) {
279 window.addEventListener('scroll', updateAll);
280 window.addEventListener('resize', updateAll);
281 window.__dbxEditorEvents = true;
282 }
283 }
284
285 items.push({ icon, target, meta });
286
287 watchTargetLayout(icon, target, container);
288 schedulePositionUpdates(icon, target);
289
290 return icon;
291 }
292
293 function getTemplateNodes(target) {
294
295 if (!target) return [];
296
297 if (Array.isArray(target.nodes) && target.nodes.length) {
298 return target.nodes;
299 }
300
301 if (target.type === 'element' && target.node) {
302 return [target.node];
303 }
304
305 if (target.type === 'text' && target.node && target.node.parentElement) {
306 return [target.node.parentElement];
307 }
308
309 return [];
310 }
311
312 function highlightTarget(target, state) {
313
314 getTemplateNodes(target).forEach(node => highlight(node, state));
315 }
316
317 function updateAll() {
318 items.forEach(({ icon, target }) => {
319 positionIcon(icon, target);
320 });
321 }
322
323 function schedulePositionUpdates(icon, target) {
324 requestAnimationFrame(() => positionIcon(icon, target));
325 [50, 150, 300, 700, 1200, 2000].forEach(delay => {
326 setTimeout(() => positionIcon(icon, target), delay);
327 });
328 }
329
330 function watchTargetLayout(icon, target, container) {
331
332 if (!window.ResizeObserver) return;
333
334 const observer = new ResizeObserver(() => {
335 requestAnimationFrame(() => positionIcon(icon, target));
336 });
337 getTemplateNodes(target).forEach(anchor => {
338 if (anchor && anchor.nodeType === 1) {
339 observer.observe(anchor);
340 }
341 });
342
343 if (container && container.nodeType === 1) {
344 observer.observe(container);
345 }
346
347 icon.__dbxResizeObserver = observer;
348 }
349
350 function positionIcon(icon, target) {
351
352 if (icon.dataset.dbxEditorInCell === '1') {
353 return;
354 }
355
356 let rect = getTargetRect(target);
357
358 if (target.originalNode) {
359 const originalRect = getTargetRect({
360 type: 'element',
361 node: target.originalNode
362 });
363
364 if (isUsableEditorRect(originalRect) && !isNonPositionableElement(target.originalNode)) {
365 target.type = 'element';
366 target.node = target.originalNode;
367 rect = originalRect;
368 } else if (!isUsableEditorRect(rect)) {
369 const anchor = isHiddenInput(target.originalNode)
370 ? findHiddenFieldAnchor(target.originalNode)
371 : findVisibleEditorAnchor(target.originalNode);
372
373 if (anchor) {
374 target.type = 'element';
375 target.node = anchor;
376 rect = getTargetRect(target);
377 }
378 }
379 }
380
381 if (!isUsableEditorRect(rect)) return;
382
383 const container = icon.parentElement;
384 icon.style.zIndex = getEditorIconZIndex(container);
385
386 const slot = getIconStackSlot(icon, target, container, rect);
387 const row = Math.floor(slot / 8);
388 const col = slot % 8;
389 const offset = row * ICON_STACK_STEP_Y;
390 const xOffset = col * ICON_STACK_STEP_X;
391 icon.dataset.dbxEditorStackSlot = String(slot);
392
393 // πŸ”₯ FIX: Header NICHT bewegen
394 if (container.closest('#dbxHeader')) {
395
396 // einmal initial setzen reicht (kein scroll-follow)
397 icon.style.top = (rect.top + offset) + 'px';
398 icon.style.left = (rect.left + xOffset) + 'px';
399 return;
400 }
401
402 // BODY (web-layout)
403 if (container === document.body) {
404
405 icon.style.top = (rect.top + window.scrollY + offset) + 'px';
406 icon.style.left = (rect.left + window.scrollX + xOffset) + 'px';
407 return;
408 }
409
410 // CONTAINER (app-layout)
411 const crect = container.getBoundingClientRect();
412
413 icon.style.top = (rect.top - crect.top + container.scrollTop + offset) + 'px';
414 icon.style.left = (rect.left - crect.left + container.scrollLeft + xOffset) + 'px';
415 }
416
417 function getIconStackSlot(icon, target, container, rect) {
418
419 const anchor = getTargetAnchorNode(target);
420 if (!anchor) return 0;
421
422 const sameTargetItems = items.filter(item => {
423 return item.icon &&
424 item.icon.parentElement === container &&
425 (
426 getTargetAnchorNode(item.target) === anchor ||
427 targetsMayOverlap(item.target, rect)
428 );
429 });
430
431 const idx = sameTargetItems.findIndex(item => item.icon === icon);
432 return idx >= 0 ? idx : 0;
433 }
434
435 function targetsMayOverlap(target, rect) {
436
437 const otherRect = getTargetRect(target);
438 if (!otherRect) return false;
439
440 return Math.abs(otherRect.left - rect.left) < ICON_COLLISION_WIDTH &&
441 Math.abs(otherRect.top - rect.top) < ICON_COLLISION_HEIGHT;
442 }
443
444 function getUnionRect(nodes) {
445
446 let top = Infinity;
447 let left = Infinity;
448 let bottom = -Infinity;
449 let right = -Infinity;
450 let found = false;
451
452 (nodes || []).forEach(node => {
453 if (!node || !node.getBoundingClientRect) return;
454
455 const rect = node.getBoundingClientRect();
456 if (rect.width <= 0 && rect.height <= 0) return;
457
458 found = true;
459 top = Math.min(top, rect.top);
460 left = Math.min(left, rect.left);
461 bottom = Math.max(bottom, rect.bottom);
462 right = Math.max(right, rect.right);
463 });
464
465 if (!found) return null;
466
467 return {
468 top: top,
469 left: left,
470 bottom: bottom,
471 right: right,
472 width: right - left,
473 height: bottom - top
474 };
475 }
476
477 function getTargetRect(target) {
478
479 if (!target) return null;
480
481 const templateNodes = getTemplateNodes(target);
482 if (templateNodes.length) {
483 const union = getUnionRect(templateNodes);
484 if (union) return union;
485 }
486
487 if (!target.node) return null;
488
489 if (target.type === 'text') {
490 const range = document.createRange();
491 range.setStart(target.node, 0);
492 range.setEnd(target.node, 1);
493 return range.getBoundingClientRect();
494 }
495
496 if (target.node.nodeType === 1 && isHiddenInput(target.node)) {
497 const anchor = findHiddenFieldAnchor(target.node);
498
499 if (anchor && anchor.getBoundingClientRect) {
500 return anchor.getBoundingClientRect();
501 }
502 }
503
504 if (!target.node.getBoundingClientRect) return null;
505 return target.node.getBoundingClientRect();
506 }
507
508 function isUsableEditorRect(rect) {
509 return !!rect &&
510 Number.isFinite(rect.top) &&
511 Number.isFinite(rect.left) &&
512 (rect.width > 0 || rect.height > 0 || rect.top > 0 || rect.left > 0);
513 }
514
515 function getTargetAnchorNode(target) {
516
517 const templateNodes = getTemplateNodes(target);
518 if (templateNodes.length) {
519 return templateNodes[0];
520 }
521
522 if (!target || !target.node) return null;
523 return target.node.nodeType === 3
524 ? (target.node.parentElement || target.node)
525 : target.node;
526 }
527
528 function isTableCellMarker(commentNode) {
529
530 const parent = commentNode && commentNode.parentElement;
531 return !!(parent && parent.matches && parent.matches('td, th'));
532 }
533
534 function isTableCellTarget(target) {
535
536 const nodes = getTemplateNodes(target);
537 return nodes.length === 1 && nodes[0].matches && nodes[0].matches('td, th');
538 }
539
540 function attachTableCellIcon(icon, target) {
541
542 const cell = getTemplateNodes(target)[0];
543 if (!cell) return;
544
545 const computed = window.getComputedStyle(cell);
546 if (computed.position === 'static') {
547 cell.dataset.dbxEditorHadPosition = '1';
548 cell.style.position = 'relative';
549 }
550
551 icon.className = ICON_CLASS + ' ' + ICON_CLASS + '--cell';
552 icon.style.position = 'absolute';
553 icon.style.top = '2px';
554 icon.style.left = '2px';
555 icon.style.right = 'auto';
556 icon.style.bottom = 'auto';
557 icon.style.zIndex = '6';
558 icon.dataset.dbxEditorInCell = '1';
559 cell.appendChild(icon);
560 }
561
562 function highlight(el, state) {
563
564 if (!el) return;
565 el.classList.toggle('dbx-editor-highlight', !!state);
566 }
567
568 function getZIndex(el) {
569 while (el && el !== document.body) {
570 const z = window.getComputedStyle(el).zIndex;
571 if (z !== 'auto') return parseInt(z, 10) || 0;
572 el = el.parentElement;
573 }
574 return 0;
575 }
576
577 function getEditorIconZIndex(container) {
578 const zIndex = getZIndex(container) + ICON_Z_INDEX_OFFSET;
579 return Math.max(ICON_Z_INDEX_MIN, zIndex);
580 }
581
582
583 function findTplEndComment(startComment) {
584
585 let depth = 1;
586 let node = startComment.nextSibling;
587
588 while (node) {
589 if (node.nodeType === 8) {
590 const txt = (node.nodeValue || '').trim();
591
592 if (txt.startsWith('DBX-TPL-START|')) {
593 depth++;
594 } else if (txt === 'DBX-TPL-END') {
595 depth--;
596 if (depth === 0) {
597 return node;
598 }
599 }
600 }
601
602 node = node.nextSibling;
603 }
604
605 return null;
606 }
607
608 function collectTplElements(startComment) {
609
610 const endComment = findTplEndComment(startComment);
611 const nodes = [];
612
613 if (!endComment) {
614 return nodes;
615 }
616
617 let node = startComment.nextSibling;
618
619 while (node && node !== endComment) {
620 if (node.nodeType === 1) {
621 nodes.push(node);
622 }
623 node = node.nextSibling;
624 }
625
626 return nodes;
627 }
628
629 function findTemplateTarget(startComment) {
630
631 const parent = startComment.parentElement;
632
633 if (parent && parent.matches && parent.matches('td, th')) {
634 return {
635 type: 'template',
636 nodes: [parent],
637 node: parent
638 };
639 }
640
641 const nodes = collectTplElements(startComment);
642
643 if (nodes.length === 1 && nodes[0].matches && nodes[0].matches('td, th')) {
644 return {
645 type: 'template',
646 nodes: nodes,
647 node: nodes[0]
648 };
649 }
650
651 if (nodes.length) {
652 return {
653 type: 'template',
654 nodes: nodes,
655 node: nodes[0]
656 };
657 }
658
659 let node = startComment.nextSibling;
660
661 while (node) {
662 if (node.nodeType === 8 && (node.nodeValue || '').trim() === 'DBX-TPL-END') {
663 break;
664 }
665
666 if (node.nodeType === 1) {
667 return {
668 type: 'template',
669 nodes: [node],
670 node: node
671 };
672 }
673
674 if (node.nodeType === 3 && node.textContent.trim() !== '') {
675 return { type: 'text', node: node, nodes: [] };
676 }
677
678 node = node.nextSibling;
679 }
680
681 if (startComment.parentElement) {
682 return {
683 type: 'template',
684 nodes: [startComment.parentElement],
685 node: startComment.parentElement
686 };
687 }
688
689 return null;
690 }
691
692 function findTargetElement(startNode) {
693
694 if (startNode.nodeType === 8) {
695 const txt = (startNode.nodeValue || '').trim();
696
697 if (txt.startsWith('DBX-TPL-START|')) {
698 return findTemplateTarget(startNode);
699 }
700 }
701
702 let node = startNode.nextSibling;
703
704 while (node) {
705
706 // TEXT mit Inhalt β†’ direkt verwenden!
707 if (node.nodeType === 3 && node.textContent.trim() !== '') {
708 return { type: 'text', node: node, nodes: [] };
709 }
710
711 // ELEMENT
712 if (node.nodeType === 1) {
713 return { type: 'element', node: node, nodes: [node] };
714 }
715
716 node = node.nextSibling;
717 }
718
719 if (startNode.parentElement) {
720 return {
721 type: 'element',
722 node: startNode.parentElement,
723 nodes: [startNode.parentElement]
724 };
725 }
726
727 return null;
728 }
729
730 function normalizeTarget(target) {
731
732 if (!target) return null;
733
734 if (target.type === 'template') {
735 return target;
736 }
737
738 if (!target.node) return null;
739
740 if (target.type === 'element' && isHiddenInput(target.node)) {
741 const anchor = findHiddenFieldAnchor(target.node);
742
743 if (anchor) {
744 return {
745 type: 'element',
746 node: anchor,
747 originalNode: target.node
748 };
749 }
750 }
751
752 if (target.type === 'element' && isNonPositionableElement(target.node)) {
753 const anchor = findVisibleEditorAnchor(target.node);
754
755 if (anchor) {
756 return {
757 type: 'element',
758 node: anchor,
759 originalNode: target.node
760 };
761 }
762 }
763
764 return target;
765 }
766
767 function isHiddenInput(node) {
768
769 if (!node || node.nodeType !== 1) return false;
770
771 const tagName = node.tagName.toLowerCase();
772 const type = (node.getAttribute('type') || '').toLowerCase();
773
774 return tagName === 'input' && type === 'hidden';
775 }
776
777 function findHiddenFieldAnchor(hiddenInput) {
778
779 const id = hiddenInput.getAttribute('id');
780
781 if (id) {
782 const label = document.querySelector('label[for="' + CSS.escape(id) + '"]');
783
784 if (label && isVisibleEditorAnchor(label)) {
785 return label;
786 }
787 }
788
789 let prev = hiddenInput.previousElementSibling;
790
791 while (prev) {
792 if (isVisibleEditorAnchor(prev)) {
793 return prev;
794 }
795
796 prev = prev.previousElementSibling;
797 }
798
799 let parent = hiddenInput.parentElement;
800
801 while (parent && parent !== document.body) {
802 if (parent.matches('form, .dbxForm_wrapper')) {
803 parent = parent.parentElement;
804 continue;
805 }
806
807 if (isVisibleEditorAnchor(parent)) {
808 return parent;
809 }
810
811 parent = parent.parentElement;
812 }
813
814 return null;
815 }
816
817 function isNonPositionableElement(node) {
818
819 if (!node || node.nodeType !== 1) return false;
820
821 if (isHiddenInput(node)) return true;
822
823 const tagName = node.tagName.toLowerCase();
824 const type = (node.getAttribute('type') || '').toLowerCase();
825
826 if (tagName === 'input' && type === 'hidden') return true;
827 if (node.hidden) return true;
828
829 const rect = node.getBoundingClientRect ? node.getBoundingClientRect() : null;
830 if (rect && rect.width === 0 && rect.height === 0) return true;
831
832 const style = window.getComputedStyle(node);
833 return style.display === 'none' || style.visibility === 'hidden';
834 }
835
836 function findVisibleEditorAnchor(node) {
837
838 const candidates = [
839 node.closest('.dbx-window-body'),
840 node.closest('#dbxContent'),
841 node.parentElement
842 ];
843
844 for (const candidate of candidates) {
845 if (isVisibleEditorAnchor(candidate)) {
846 return candidate;
847 }
848 }
849
850 return document.body;
851 }
852
853 function isVisibleEditorAnchor(node) {
854
855 if (!node || node.nodeType !== 1 || !node.getBoundingClientRect) return false;
856
857 const rect = node.getBoundingClientRect();
858 if (rect.width <= 0 || rect.height <= 0) return false;
859
860 const style = window.getComputedStyle(node);
861 return style.display !== 'none' && style.visibility !== 'hidden';
862 }
863
864 function parseMarker(txt) {
865
866 if (txt.startsWith('DBX-TPL-START|')) {
867 return {
868 kind: 'tpl',
869 path: txt.split('|')[1] || ''
870 };
871 }
872
873 if (txt.startsWith('DBX-EDITOR|')) {
874 const parts = txt.split('|');
875 return {
876 kind: (parts[1] || 'file').toLowerCase(),
877 path: parts.slice(2).join('|') || ''
878 };
879 }
880
881 return null;
882 }
883
884 });
885
886 // ==================================================
887 // FILE MENU
888 // ==================================================
889
890 function collectEditorFiles() {
891
892 let hasAllResourcesMode = getDbxEditMode() === 9;
893
894 document.querySelectorAll('.dbx-editor-files-data').forEach(node => {
895 if (node.__dbxEditorFilesRead) return;
896 node.__dbxEditorFilesRead = true;
897
898 let payload = null;
899
900 try {
901 payload = JSON.parse(node.textContent || '{}');
902 } catch (e) {
903 warn('invalid editor files payload', e);
904 return;
905 }
906
907 if (parseInt(payload.mode, 10) === 9) {
908 hasAllResourcesMode = true;
909 }
910
911 const files = Array.isArray(payload.files) ? payload.files : [];
912
913 files.forEach(file => {
914 if (!file || !file.kind || !file.file) return;
915 const key = file.kind + '|' + file.file;
916 editorMenuFiles.set(key, {
917 kind: file.kind,
918 path: file.file
919 });
920 });
921 });
922
923 if (hasAllResourcesMode) {
924 collectLoadedCssFiles();
925 collectLoadedJsFiles();
926 }
927 }
928
929 function collectLoadedCssFiles() {
930
931 const hrefs = new Set();
932
933 Array.from(document.styleSheets || []).forEach(sheet => {
934 if (sheet && sheet.href) {
935 hrefs.add(sheet.href);
936 }
937 });
938
939 document.querySelectorAll('link[rel~="stylesheet"][href]').forEach(link => {
940 hrefs.add(link.href);
941 });
942
943 hrefs.forEach(href => {
944 const path = stylesheetHrefToEditorPath(href);
945 if (!path) return;
946
947 editorMenuFiles.set('css|' + path, {
948 kind: 'css',
949 path
950 });
951 });
952 }
953
954 function stylesheetHrefToEditorPath(href) {
955
956 if (!href || href.indexOf('data:') === 0 || href.indexOf('blob:') === 0) {
957 return '';
958 }
959
960 let url;
961
962 try {
963 url = new URL(href, window.location.href);
964 } catch (e) {
965 return '';
966 }
967
968 if (url.origin !== window.location.origin) {
969 return '';
970 }
971
972 let path = url.pathname.replace(/\\/g, '/').replace(/^\/+/, '');
973 const appRoot = window.location.pathname.replace(/\\/g, '/').split('/').filter(Boolean)[0] || '';
974
975 if (appRoot && path.indexOf(appRoot + '/') === 0) {
976 path = path.substring(appRoot.length + 1);
977 }
978
979 if (!/\.css$/i.test(path)) {
980 return '';
981 }
982
983 return path;
984 }
985
986 function collectLoadedJsFiles() {
987
988 document.querySelectorAll('script[src]').forEach(script => {
989 const path = assetHrefToEditorPath(script.src, 'js');
990 if (!path) return;
991
992 editorMenuFiles.set('js|' + path, {
993 kind: 'js',
994 path
995 });
996 });
997 }
998
999 function assetHrefToEditorPath(href, ext) {
1000
1001 if (!href || href.indexOf('data:') === 0 || href.indexOf('blob:') === 0) {
1002 return '';
1003 }
1004
1005 let url;
1006
1007 try {
1008 url = new URL(href, window.location.href);
1009 } catch (e) {
1010 return '';
1011 }
1012
1013 if (url.origin !== window.location.origin) {
1014 return '';
1015 }
1016
1017 let path = url.pathname.replace(/\\/g, '/').replace(/^\/+/, '');
1018 const appRoot = window.location.pathname.replace(/\\/g, '/').split('/').filter(Boolean)[0] || '';
1019
1020 if (appRoot && path.indexOf(appRoot + '/') === 0) {
1021 path = path.substring(appRoot.length + 1);
1022 }
1023
1024 if (!new RegExp('\\.' + ext + '$', 'i').test(path)) {
1025 return '';
1026 }
1027
1028 return path;
1029 }
1030
1031 function getDbxEditMode() {
1032
1033 const value = new URLSearchParams(window.location.search).get('dbx_edit');
1034 return parseInt(value || '0', 10) || 0;
1035 }
1036
1037 function getEditorFileUrl(file) {
1038
1039 const kind = String((file && file.kind) || '').toLowerCase();
1040 const path = String((file && file.path) || '').replace(/\\/g, '/');
1041 const requestPath = window.location.origin + (window.location.pathname || '/');
1042
1043 if (kind === 'dd') {
1044 const match = path.match(/^dbx\/modules\/([^/]+)\/dd\/([^/]+)\.dd\.php$/);
1045 if (match) {
1046 return requestPath + '?dbx_modul=dbxAdmin&dbx_run1=edit_dd&modul=' +
1047 encodeURIComponent(match[1]) +
1048 '&dd=' + encodeURIComponent(match[2]);
1049 }
1050 }
1051
1052 if (kind === 'fd') {
1053 const match = path.match(/^dbx\/modules\/([^/]+)\/fd\/([^/]+)\.fd\.php$/);
1054 if (match) {
1055 return requestPath + '?dbx_modul=dbxAdmin&dbx_run1=edit_fd&modul=' +
1056 encodeURIComponent(match[1]) +
1057 '&fd=' + encodeURIComponent(match[2]);
1058 }
1059 }
1060
1061 return requestPath + '?dbx_modul=dbxEditor&dbx_run1=edit&file=' + encodeURIComponent(path);
1062 }
1063
1064 function watchEditorFilesData() {
1065
1066 if (window.__dbxEditorFilesObserver) return;
1067
1068 const observer = new MutationObserver(mutations => {
1069 if (window.__dbxEditorFilesRendering) return;
1070
1071 const hasExternalEditorDataChange = mutations.some(mutation => {
1072 const target = mutation.target;
1073
1074 if (target && target.closest && target.closest('#dbxEditorFilesMenu')) {
1075 return false;
1076 }
1077
1078 return Array.from(mutation.addedNodes || []).some(node => {
1079 if (!node) return false;
1080 if (node.id === 'dbxEditorFilesMenu') return false;
1081 if (node.closest && node.closest('#dbxEditorFilesMenu')) return false;
1082 if (node.matches && node.matches('.dbx-editor-files-data')) return true;
1083 if (node.querySelector && node.querySelector('.dbx-editor-files-data')) return true;
1084 return false;
1085 });
1086 });
1087
1088 if (!hasExternalEditorDataChange) return;
1089
1090 collectEditorFiles();
1091 renderEditorFilesMenu();
1092 });
1093
1094 observer.observe(document.body, {
1095 childList: true,
1096 subtree: true
1097 });
1098
1099 window.__dbxEditorFilesObserver = observer;
1100 }
1101
1102 function renderEditorFilesMenu() {
1103
1104 let menu = document.getElementById('dbxEditorFilesMenu');
1105
1106 if (!editorMenuFiles.size) {
1107 if (menu) {
1108 menu.remove();
1109 syncEditorFilesMenuLayer(null);
1110 }
1111 return;
1112 }
1113
1114 if (menu && menu.tagName.toLowerCase() !== 'li') {
1115 menu.remove();
1116 menu = null;
1117 }
1118
1119 if (!menu) {
1120 menu = document.createElement('li');
1121 menu.id = 'dbxEditorFilesMenu';
1122 menu.className = 'align-right dbx-menu-right dbx-editor-files-menu dbx-menu-item has-children';
1123 }
1124 if (!attachEditorMenu(menu)) {
1125 return;
1126 }
1127
1128 const files = sortEditorFiles(Array.from(editorMenuFiles.values()));
1129 const signature = JSON.stringify(files);
1130
1131 if (menu.dataset.dbxEditorFilesSignature === signature) {
1132 return;
1133 }
1134
1135 const grouped = {};
1136
1137 files.forEach(file => {
1138 const kind = file.kind || 'file';
1139 grouped[kind] = grouped[kind] || [];
1140 grouped[kind].push(file);
1141 });
1142
1143 let html = '<a class="dbx-menu-link dbx-editor-files-toggle" data-role="toggle" aria-haspopup="true" aria-expanded="false" title="Verwendete Editor-Dateien">';
1144 html += '<i class="bi bi-files"></i><span>' + files.length + '</span><span class="dbx-caret"></span>';
1145 html += '</a>';
1146 html += '<ul class="dbx-menu-list dbx-editor-files-list">';
1147
1148 getEditorKindOrder(grouped).forEach(kind => {
1149 html += '<li class="dbx-menu-item dbx-editor-files-title">' + escapeHtml(getMarkerTitle({ kind })) + '</li>';
1150
1151 grouped[kind].forEach(file => {
1152 const url = getEditorFileUrl(file);
1153 html += '<li class="dbx-menu-item">';
1154 html += '<a class="dbx-menu-link" href="' + url + '" data-dbx="lib=openWin|url=' + url + '|title=' + encodeDataValue(getMarkerTitle(file)) + '|reload=1|width=80%|height=80%|left=center|top=center|prio=last|minimizable=1|maximizable=1">';
1155 html += '<i class="bi ' + getMarkerIcon(file) + '"></i>';
1156 html += '<span>' + escapeHtml(getShortPath(file.path)) + '</span>';
1157 html += '</a>';
1158 html += '</li>';
1159 });
1160 });
1161
1162 html += '</ul>';
1163 window.__dbxEditorFilesRendering = true;
1164
1165 try {
1166 menu.dataset.dbxEditorFilesSignature = signature;
1167 menu.innerHTML = html;
1168
1169 bindEditorMenu(menu);
1170
1171 dbx.rescan(menu);
1172 } finally {
1173 window.__dbxEditorFilesRendering = false;
1174 }
1175 }
1176
1177 function attachEditorMenu(menu) {
1178
1179 const slot = getEditorMenuSlot();
1180
1181 if (slot) {
1182 const parent = slot.parentElement;
1183 const spacer = parent && parent.querySelector(':scope > .dbx-menu-spacer');
1184
1185 if (menu.parentElement !== parent) {
1186 slot.insertAdjacentElement('afterend', menu);
1187 } else if (menu.previousElementSibling !== slot) {
1188 slot.insertAdjacentElement('afterend', menu);
1189 }
1190
1191 if (spacer && (spacer.compareDocumentPosition(menu) & Node.DOCUMENT_POSITION_PRECEDING)) {
1192 slot.insertAdjacentElement('afterend', menu);
1193 }
1194
1195 menu.classList.add('dbx-menu-right');
1196 menu.style.display = '';
1197 return true;
1198 }
1199
1200 if (menu.parentElement) {
1201 menu.remove();
1202 }
1203
1204 return false;
1205 }
1206
1207 function getEditorMenuSlot() {
1208
1209 const slots = Array.from(document.querySelectorAll('#dbx_admin_menu .dbx-edit-menu-slot, .dbx-menu-admin .dbx-edit-menu-slot, .dbx-edit-menu-slot'));
1210
1211 if (!slots.length) return null;
1212
1213 return slots.find(slot => {
1214 const rect = slot.getBoundingClientRect();
1215 const style = window.getComputedStyle(slot);
1216 return style.display !== 'none' && style.visibility !== 'hidden' && rect.width > 0 && rect.height > 0;
1217 }) || slots[0];
1218 }
1219
1220 function bindEditorMenu(menu) {
1221
1222 const toggle = menu.querySelector('.dbx-editor-files-toggle');
1223
1224 if (toggle) {
1225 toggle.setAttribute('data-role', 'toggle');
1226 toggle.setAttribute('aria-haspopup', 'true');
1227 toggle.setAttribute('aria-expanded', menu.classList.contains('is-open') ? 'true' : 'false');
1228 }
1229
1230 if (!menu.__dbxEditorFilesLayerObserver && window.MutationObserver) {
1231 menu.__dbxEditorFilesLayerObserver = new MutationObserver(() => syncEditorFilesMenuLayer(menu));
1232 menu.__dbxEditorFilesLayerObserver.observe(menu, {
1233 attributes: true,
1234 attributeFilter: ['class']
1235 });
1236 }
1237
1238 syncEditorFilesMenuLayer(menu);
1239
1240 if (!window.__dbxEditorMenuCloseBound) {
1241 document.addEventListener('click', event => {
1242 const openMenu = document.querySelector('.dbx-editor-files-menu.is-open');
1243 if (!openMenu) return;
1244 if (openMenu.contains(event.target)) return;
1245 openMenu.classList.remove('is-open');
1246 syncEditorFilesMenuLayer(openMenu);
1247 });
1248
1249 window.__dbxEditorMenuCloseBound = true;
1250 }
1251
1252 menu.onclick = event => {
1253 const link = event.target.closest('a');
1254 if (!link) return;
1255 if (link.classList.contains('dbx-editor-files-toggle')) return;
1256 menu.classList.remove('is-open');
1257 syncEditorFilesMenuLayer(menu);
1258 };
1259 }
1260
1261 function syncEditorFilesMenuLayer(menu) {
1262
1263 const open = !!(menu && menu.classList.contains('is-open'));
1264 const body = document.body;
1265 const header = document.getElementById('dbxHeader');
1266
1267 if (body) {
1268 body.classList.toggle('dbx-editor-files-menu-open', open);
1269 }
1270
1271 if (header) {
1272 header.classList.toggle('dbx-editor-files-menu-open', open);
1273 }
1274
1275 if (menu) {
1276 const toggle = menu.querySelector('.dbx-editor-files-toggle');
1277 if (toggle) {
1278 toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
1279 }
1280 }
1281 }
1282
1283 function sortEditorFiles(files) {
1284
1285 const order = {
1286 fd: 10,
1287 dd: 20,
1288 class: 30,
1289 sysclass: 40,
1290 config: 45,
1291 css: 48,
1292 js: 49,
1293 tpl: 50,
1294 file: 60,
1295 design: 100
1296 };
1297
1298 return files.sort((a, b) => {
1299 const kindA = (a.kind || 'file').toLowerCase();
1300 const kindB = (b.kind || 'file').toLowerCase();
1301 const rankA = order[kindA] || 99;
1302 const rankB = order[kindB] || 99;
1303
1304 if (rankA !== rankB) return rankA - rankB;
1305
1306 return getShortPath(a.path || '').localeCompare(getShortPath(b.path || ''), undefined, {
1307 sensitivity: 'base'
1308 });
1309 });
1310 }
1311
1312 function getEditorKindOrder(grouped) {
1313
1314 const preferred = ['fd', 'dd', 'class', 'sysclass', 'config', 'css', 'js', 'tpl', 'file', 'design'];
1315 const existing = Object.keys(grouped);
1316 const ordered = preferred.filter(kind => grouped[kind]);
1317
1318 existing.forEach(kind => {
1319 if (!ordered.includes(kind)) {
1320 ordered.push(kind);
1321 }
1322 });
1323
1324 return ordered;
1325 }
1326
1327 function getShortPath(path) {
1328
1329 const parts = String(path || '').split('/');
1330 const normalized = parts.join('/');
1331 const prefix = 'dbx/modules/';
1332
1333 if (normalized.indexOf(prefix) === 0) {
1334 return normalized.substring(prefix.length);
1335 }
1336
1337 if (normalized.indexOf('dbx/design/') === 0) {
1338 return normalized.substring('dbx/'.length);
1339 }
1340
1341 return normalized;
1342 }
1343
1344 function escapeHtml(value) {
1345
1346 const div = document.createElement('div');
1347 div.textContent = String(value || '');
1348 return div.innerHTML;
1349 }
1350
1351 function encodeDataValue(value) {
1352
1353 return encodeURIComponent(String(value || ''));
1354 }
1355
1356 function getMarkerIcon(meta) {
1357
1358 return getFileIcon(meta.path || '');
1359 }
1360
1361 function getMarkerTitle(meta) {
1362
1363 const kind = (meta.kind || 'tpl').toLowerCase();
1364
1365 if (kind === 'fd') return 'FD Definition';
1366 if (kind === 'dd') return 'DD Definition';
1367 if (kind === 'class') return 'Modul Class';
1368 if (kind === 'sysclass') return 'myX System Class';
1369 if (kind === 'config') return meta.path ? getConfigTitle(meta.path) : 'Config';
1370 if (kind === 'css') return meta.path ? getCssTitle(meta.path) : 'CSS';
1371 if (kind === 'js') return meta.path ? getJsTitle(meta.path) : 'JS';
1372 if (kind === 'tpl') return meta.path ? getTemplateTitle(meta.path) : 'Template';
1373 if (kind === 'design') return meta.path ? getDesignTitle(meta.path) : 'Design Page';
1374
1375 return meta.path ? getGenericFileTitle(meta.path) : 'Datei';
1376 }
1377
1378 function getTemplateOverlayTitle(path) {
1379
1380 const normalized = String(path || '').replace(/\\/g, '/');
1381 const parts = normalized.split('/').filter(Boolean);
1382 const tplIndex = parts.lastIndexOf('tpl');
1383 const modulesIndex = parts.lastIndexOf('modules');
1384
1385 const modul = modulesIndex !== -1 && parts[modulesIndex + 1]
1386 ? parts[modulesIndex + 1]
1387 : (tplIndex > 0 ? parts[tplIndex - 1] : 'dbx');
1388
1389 let templateName = 'Template';
1390
1391 if (tplIndex !== -1 && parts[tplIndex + 2]) {
1392 templateName = splitFileName(parts[tplIndex + 2]).name || parts[tplIndex + 2];
1393 } else if (parts.length) {
1394 templateName = splitFileName(parts[parts.length - 1]).name || parts[parts.length - 1];
1395 }
1396
1397 return modul + '|' + templateName;
1398 }
1399
1400 function getTemplateTitle(path) {
1401
1402 const normalized = String(path || '').replace(/\\/g, '/');
1403 const parts = normalized.split('/').filter(Boolean);
1404 const tplIndex = parts.lastIndexOf('tpl');
1405 const modulesIndex = parts.lastIndexOf('modules');
1406
1407 if (tplIndex !== -1 && parts[tplIndex + 1] && parts[tplIndex + 2]) {
1408 const modul = modulesIndex !== -1 && parts[modulesIndex + 1]
1409 ? parts[modulesIndex + 1]
1410 : (parts[tplIndex - 1] || 'dbx');
1411 const type = parts[tplIndex + 1];
1412 const base = parts[tplIndex + 2];
1413 const parsed = splitFileName(base);
1414 return [modul, parsed.name, (parsed.ext || type)].filter(Boolean).join(' - ');
1415 }
1416
1417 const base = parts.length ? parts[parts.length - 1] : normalized;
1418 const parsed = splitFileName(base);
1419 return [parsed.name || 'Template', parsed.ext].filter(Boolean).join(' - ');
1420 }
1421
1422 function getDesignTitle(path) {
1423
1424 const normalized = String(path || '').replace(/\\/g, '/');
1425 const parts = normalized.split('/').filter(Boolean);
1426 const designIndex = parts.lastIndexOf('design');
1427 const design = designIndex !== -1 && parts[designIndex + 1]
1428 ? parts[designIndex + 1]
1429 : 'Design';
1430 const type = designIndex !== -1 && parts[designIndex + 2]
1431 ? parts[designIndex + 2]
1432 : '';
1433 const base = parts.length ? parts[parts.length - 1] : normalized;
1434 const parsed = splitFileName(base);
1435
1436 return ['Design Page', design, parsed.name, (parsed.ext || type)].filter(Boolean).join(' - ');
1437 }
1438
1439 function getCssTitle(path) {
1440
1441 const normalized = String(path || '').replace(/\\/g, '/');
1442 const parts = normalized.split('/').filter(Boolean);
1443 const base = parts.length ? parts[parts.length - 1] : normalized;
1444 const parsed = splitFileName(base);
1445
1446 return ['CSS', parsed.name, parsed.ext].filter(Boolean).join(' - ');
1447 }
1448
1449 function getJsTitle(path) {
1450
1451 const normalized = String(path || '').replace(/\\/g, '/');
1452 const parts = normalized.split('/').filter(Boolean);
1453 const base = parts.length ? parts[parts.length - 1] : normalized;
1454 const parsed = splitFileName(base);
1455
1456 return ['JS', parsed.name, parsed.ext].filter(Boolean).join(' - ');
1457 }
1458
1459 function getConfigTitle(path) {
1460
1461 const normalized = String(path || '').replace(/\\/g, '/');
1462 const parts = normalized.split('/').filter(Boolean);
1463 const modulesIndex = parts.lastIndexOf('modules');
1464 const cfgIndex = parts.lastIndexOf('cfg');
1465 const base = parts.length ? parts[parts.length - 1] : normalized;
1466 const parsed = splitFileName(base);
1467
1468 const modul = modulesIndex !== -1 && parts[modulesIndex + 1]
1469 ? parts[modulesIndex + 1]
1470 : (cfgIndex > 0 ? parts[cfgIndex - 1] : '');
1471
1472 const name = parsed.name === 'config' ? 'config' : (parsed.name || 'config');
1473 return [modul, name, parsed.ext].filter(Boolean).join(' - ');
1474 }
1475
1476 function getGenericFileTitle(path) {
1477
1478 const normalized = String(path || '').replace(/\\/g, '/');
1479 const parts = normalized.split('/').filter(Boolean);
1480 const base = parts.length ? parts[parts.length - 1] : normalized;
1481 const parsed = splitFileName(base);
1482
1483 return [parsed.name || 'Datei', parsed.ext].filter(Boolean).join(' - ');
1484 }
1485
1486 function splitFileName(fileName) {
1487
1488 const base = String(fileName || '');
1489 const dot = base.lastIndexOf('.');
1490
1491 if (dot > 0 && dot < base.length - 1) {
1492 return {
1493 name: base.substring(0, dot),
1494 ext: base.substring(dot + 1)
1495 };
1496 }
1497
1498 return {
1499 name: base,
1500 ext: ''
1501 };
1502 }
1503
1504 function getMarkerOffset(kind) {
1505
1506 kind = (kind || 'tpl').toLowerCase();
1507
1508 if (kind === 'fd') return 28;
1509 if (kind === 'dd') return 56;
1510 if (kind === 'class') return 84;
1511 if (kind === 'sysclass') return 112;
1512 if (kind === 'config') return 140;
1513
1514 return 0;
1515 }
1516
1517 function getFileIcon(path) {
1518
1519 const ext = (path.split('.').pop() || '').toLowerCase();
1520
1521 if (ext === 'php') return 'bi-filetype-php';
1522 if (ext === 'js') return 'bi-filetype-js';
1523 if (ext === 'css') return 'bi-filetype-css';
1524 if (ext === 'html' || ext === 'htm') return 'bi-filetype-html';
1525
1526 return 'bi-file-earmark';
1527 }
1528
1529 }
1530
1531 });
1532
1533})();