3 if (!window.dbx) return;
4 const dbx = window.dbx;
6 function log(...a) { dbx.log('[icons_editor]', 'β', ...a); }
7 function warn(...a) { dbx.warn('[icons_editor]', 'β', ...a); }
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;
17 dbx.feature.register("icons_editor", {
19 scope: "global", // π₯ FIX
22 // π₯ NEU: CSS deklarativ (PREPARE ΓΌbernimmt)
24 ['css', 'design', 'c-icons_editor.css']
29 log("init start", cfg);
31 const root = document.body;
32 const editorMenuFiles = window.__dbxEditorMenuFiles || new Map();
33 window.__dbxEditorMenuFiles = editorMenuFiles;
36 renderEditorFilesMenu();
37 watchEditorFilesData();
39 // --------------------------------------------------
40 // INIT GUARD (auf Core-Standard gebracht)
41 // --------------------------------------------------
43 el.__dbxInitialized = el.__dbxInitialized || {};
44 if (el.__dbxInitialized["icons_editor"]) {
45 log("already initialized on element, skip");
48 el.__dbxInitialized["icons_editor"] = true;
50 // --------------------------------------------------
51 // DEPENDENCY (kein dbx.load mehr!)
52 // --------------------------------------------------
54 log("resolve deps: openWin");
56 dbx.resolveFeature("openWin", function () {
62 // --------------------------------------------------
64 // --------------------------------------------------
66 dbx.iconsEditor = dbx.iconsEditor || {};
67 dbx.iconsEditor.rescan = function (scanRoot) {
68 runWhenRendered(() => scanEditorMarkers(scanRoot));
70 this.rescan = dbx.iconsEditor.rescan;
72 runWhenRendered(() => scanEditorMarkers(root));
74 function runWhenRendered(callback) {
76 requestAnimationFrame(() => {
77 requestAnimationFrame(() => {
78 setTimeout(callback, 0);
83 if (document.readyState === 'complete') {
88 window.addEventListener('load', run, { once: true });
91 function scanEditorMarkers(scanRoot) {
92 const scope = scanRoot || document.body;
95 renderEditorFilesMenu();
97 log("scanning DOM (scope)", scope);
101 const walker = document.createTreeWalker(
103 NodeFilter.SHOW_COMMENT,
110 while ((node = walker.nextNode())) {
111 if (node.__dbxEditorIconCreated) {
115 const txt = node.nodeValue.trim();
116 const marker = parseMarker(txt);
119 comments.push({ node, marker });
123 log("found comments:", comments.length);
125 // --------------------------------------------------
127 // --------------------------------------------------
129 const seenTplPaths = new Set();
130 const seenTplTableCols = new Set();
132 comments.forEach(item => {
134 const node = item.node;
135 const meta = item.marker;
136 const path = meta.path || '';
138 // Nur Template-HTML-Editor (ποΈ HTML bearbeiten).
139 if (meta.kind !== 'tpl') {
140 node.__dbxEditorIconCreated = true;
144 const inTableCell = isTableCellMarker(node);
146 if (path && inTableCell) {
147 const cell = node.parentElement;
148 const colKey = path + '|' + (cell ? cell.cellIndex : 0);
150 // Pro Tabellenspalte ein Stift (erste Zeile im DOM).
151 if (seenTplTableCols.has(colKey)) {
152 node.__dbxEditorIconCreated = true;
155 seenTplTableCols.add(colKey);
157 if (seenTplPaths.has(path)) {
158 node.__dbxEditorIconCreated = true;
161 seenTplPaths.add(path);
164 const target = normalizeTarget(findTargetElement(node));
167 warn("no target found for", path);
171 createIcon(target, meta);
172 node.__dbxEditorIconCreated = true;
176 // --------------------------------------------------
178 // --------------------------------------------------
180 if (!window.__dbxEditorEvents) {
182 log("attach global events");
184 window.addEventListener('scroll', updateAll);
185 window.addEventListener('resize', updateAll);
186 window.addEventListener('load', updateAll);
188 window.__dbxEditorEvents = true;
191 log("initialized, total icons:", items.length);
193 // ==================================================
195 // ==================================================
197 function createIcon(target, meta) {
199 const url = getEditorFileUrl(meta);
201 const icon = document.createElement('a');
204 const title = getTemplateOverlayTitle(meta.path || '');
206 icon.innerHTML = '<span title="' + escapeHtml(title) + '">ποΈ</span>';
207 icon.className = ICON_CLASS;
214 '|title=' + encodeDataValue(title) +
224 icon.addEventListener('mouseenter', () => highlightTarget(target, true));
225 icon.addEventListener('mouseleave', () => highlightTarget(target, false));
227 icon.style.cursor = 'pointer';
228 icon.style.textDecoration = 'none';
230 if (isTableCellTarget(target)) {
231 attachTableCellIcon(icon, target);
233 items.push({ icon, target, meta });
234 watchTargetLayout(icon, target, getTemplateNodes(target)[0]);
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;
244 icon.style.position = 'absolute';
245 icon.style.zIndex = getEditorIconZIndex(container);
248 container.appendChild(icon);
252 // π₯π₯ FINAL FIX: echten Scroll-Container sauber bestimmen
254 while (scrollEl && scrollEl !== document.body) {
255 const style = window.getComputedStyle(scrollEl);
258 style.overflowY === 'auto' ||
259 style.overflowY === 'scroll'
264 scrollEl = scrollEl.parentElement;
267 // π FALL 1: app-layout (container scroll)
268 if (scrollEl && scrollEl !== document.body) {
270 if (!scrollEl.__dbxScrollBound) {
271 scrollEl.addEventListener('scroll', updateAll);
272 scrollEl.__dbxScrollBound = true;
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;
285 items.push({ icon, target, meta });
287 watchTargetLayout(icon, target, container);
288 schedulePositionUpdates(icon, target);
293 function getTemplateNodes(target) {
295 if (!target) return [];
297 if (Array.isArray(target.nodes) && target.nodes.length) {
301 if (target.type === 'element' && target.node) {
302 return [target.node];
305 if (target.type === 'text' && target.node && target.node.parentElement) {
306 return [target.node.parentElement];
312 function highlightTarget(target, state) {
314 getTemplateNodes(target).forEach(node => highlight(node, state));
317 function updateAll() {
318 items.forEach(({ icon, target }) => {
319 positionIcon(icon, target);
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);
330 function watchTargetLayout(icon, target, container) {
332 if (!window.ResizeObserver) return;
334 const observer = new ResizeObserver(() => {
335 requestAnimationFrame(() => positionIcon(icon, target));
337 getTemplateNodes(target).forEach(anchor => {
338 if (anchor && anchor.nodeType === 1) {
339 observer.observe(anchor);
343 if (container && container.nodeType === 1) {
344 observer.observe(container);
347 icon.__dbxResizeObserver = observer;
350 function positionIcon(icon, target) {
352 if (icon.dataset.dbxEditorInCell === '1') {
356 let rect = getTargetRect(target);
358 if (target.originalNode) {
359 const originalRect = getTargetRect({
361 node: target.originalNode
364 if (isUsableEditorRect(originalRect) && !isNonPositionableElement(target.originalNode)) {
365 target.type = 'element';
366 target.node = target.originalNode;
368 } else if (!isUsableEditorRect(rect)) {
369 const anchor = isHiddenInput(target.originalNode)
370 ? findHiddenFieldAnchor(target.originalNode)
371 : findVisibleEditorAnchor(target.originalNode);
374 target.type = 'element';
375 target.node = anchor;
376 rect = getTargetRect(target);
381 if (!isUsableEditorRect(rect)) return;
383 const container = icon.parentElement;
384 icon.style.zIndex = getEditorIconZIndex(container);
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);
393 // π₯ FIX: Header NICHT bewegen
394 if (container.closest('#dbxHeader')) {
396 // einmal initial setzen reicht (kein scroll-follow)
397 icon.style.top = (rect.top + offset) + 'px';
398 icon.style.left = (rect.left + xOffset) + 'px';
403 if (container === document.body) {
405 icon.style.top = (rect.top + window.scrollY + offset) + 'px';
406 icon.style.left = (rect.left + window.scrollX + xOffset) + 'px';
410 // CONTAINER (app-layout)
411 const crect = container.getBoundingClientRect();
413 icon.style.top = (rect.top - crect.top + container.scrollTop + offset) + 'px';
414 icon.style.left = (rect.left - crect.left + container.scrollLeft + xOffset) + 'px';
417 function getIconStackSlot(icon, target, container, rect) {
419 const anchor = getTargetAnchorNode(target);
420 if (!anchor) return 0;
422 const sameTargetItems = items.filter(item => {
424 item.icon.parentElement === container &&
426 getTargetAnchorNode(item.target) === anchor ||
427 targetsMayOverlap(item.target, rect)
431 const idx = sameTargetItems.findIndex(item => item.icon === icon);
432 return idx >= 0 ? idx : 0;
435 function targetsMayOverlap(target, rect) {
437 const otherRect = getTargetRect(target);
438 if (!otherRect) return false;
440 return Math.abs(otherRect.left - rect.left) < ICON_COLLISION_WIDTH &&
441 Math.abs(otherRect.top - rect.top) < ICON_COLLISION_HEIGHT;
444 function getUnionRect(nodes) {
448 let bottom = -Infinity;
449 let right = -Infinity;
452 (nodes || []).forEach(node => {
453 if (!node || !node.getBoundingClientRect) return;
455 const rect = node.getBoundingClientRect();
456 if (rect.width <= 0 && rect.height <= 0) return;
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);
465 if (!found) return null;
477 function getTargetRect(target) {
479 if (!target) return null;
481 const templateNodes = getTemplateNodes(target);
482 if (templateNodes.length) {
483 const union = getUnionRect(templateNodes);
484 if (union) return union;
487 if (!target.node) return null;
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();
496 if (target.node.nodeType === 1 && isHiddenInput(target.node)) {
497 const anchor = findHiddenFieldAnchor(target.node);
499 if (anchor && anchor.getBoundingClientRect) {
500 return anchor.getBoundingClientRect();
504 if (!target.node.getBoundingClientRect) return null;
505 return target.node.getBoundingClientRect();
508 function isUsableEditorRect(rect) {
510 Number.isFinite(rect.top) &&
511 Number.isFinite(rect.left) &&
512 (rect.width > 0 || rect.height > 0 || rect.top > 0 || rect.left > 0);
515 function getTargetAnchorNode(target) {
517 const templateNodes = getTemplateNodes(target);
518 if (templateNodes.length) {
519 return templateNodes[0];
522 if (!target || !target.node) return null;
523 return target.node.nodeType === 3
524 ? (target.node.parentElement || target.node)
528 function isTableCellMarker(commentNode) {
530 const parent = commentNode && commentNode.parentElement;
531 return !!(parent && parent.matches && parent.matches('td, th'));
534 function isTableCellTarget(target) {
536 const nodes = getTemplateNodes(target);
537 return nodes.length === 1 && nodes[0].matches && nodes[0].matches('td, th');
540 function attachTableCellIcon(icon, target) {
542 const cell = getTemplateNodes(target)[0];
545 const computed = window.getComputedStyle(cell);
546 if (computed.position === 'static') {
547 cell.dataset.dbxEditorHadPosition = '1';
548 cell.style.position = 'relative';
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);
562 function highlight(el, state) {
565 el.classList.toggle('dbx-editor-highlight', !!state);
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;
577 function getEditorIconZIndex(container) {
578 const zIndex = getZIndex(container) + ICON_Z_INDEX_OFFSET;
579 return Math.max(ICON_Z_INDEX_MIN, zIndex);
583 function findTplEndComment(startComment) {
586 let node = startComment.nextSibling;
589 if (node.nodeType === 8) {
590 const txt = (node.nodeValue || '').trim();
592 if (txt.startsWith('DBX-TPL-START|')) {
594 } else if (txt === 'DBX-TPL-END') {
602 node = node.nextSibling;
608 function collectTplElements(startComment) {
610 const endComment = findTplEndComment(startComment);
617 let node = startComment.nextSibling;
619 while (node && node !== endComment) {
620 if (node.nodeType === 1) {
623 node = node.nextSibling;
629 function findTemplateTarget(startComment) {
631 const parent = startComment.parentElement;
633 if (parent && parent.matches && parent.matches('td, th')) {
641 const nodes = collectTplElements(startComment);
643 if (nodes.length === 1 && nodes[0].matches && nodes[0].matches('td, th')) {
659 let node = startComment.nextSibling;
662 if (node.nodeType === 8 && (node.nodeValue || '').trim() === 'DBX-TPL-END') {
666 if (node.nodeType === 1) {
674 if (node.nodeType === 3 && node.textContent.trim() !== '') {
675 return { type: 'text', node: node, nodes: [] };
678 node = node.nextSibling;
681 if (startComment.parentElement) {
684 nodes: [startComment.parentElement],
685 node: startComment.parentElement
692 function findTargetElement(startNode) {
694 if (startNode.nodeType === 8) {
695 const txt = (startNode.nodeValue || '').trim();
697 if (txt.startsWith('DBX-TPL-START|')) {
698 return findTemplateTarget(startNode);
702 let node = startNode.nextSibling;
706 // TEXT mit Inhalt β direkt verwenden!
707 if (node.nodeType === 3 && node.textContent.trim() !== '') {
708 return { type: 'text', node: node, nodes: [] };
712 if (node.nodeType === 1) {
713 return { type: 'element', node: node, nodes: [node] };
716 node = node.nextSibling;
719 if (startNode.parentElement) {
722 node: startNode.parentElement,
723 nodes: [startNode.parentElement]
730 function normalizeTarget(target) {
732 if (!target) return null;
734 if (target.type === 'template') {
738 if (!target.node) return null;
740 if (target.type === 'element' && isHiddenInput(target.node)) {
741 const anchor = findHiddenFieldAnchor(target.node);
747 originalNode: target.node
752 if (target.type === 'element' && isNonPositionableElement(target.node)) {
753 const anchor = findVisibleEditorAnchor(target.node);
759 originalNode: target.node
767 function isHiddenInput(node) {
769 if (!node || node.nodeType !== 1) return false;
771 const tagName = node.tagName.toLowerCase();
772 const type = (node.getAttribute('type') || '').toLowerCase();
774 return tagName === 'input' && type === 'hidden';
777 function findHiddenFieldAnchor(hiddenInput) {
779 const id = hiddenInput.getAttribute('id');
782 const label = document.querySelector('label[for="' + CSS.escape(id) + '"]');
784 if (label && isVisibleEditorAnchor(label)) {
789 let prev = hiddenInput.previousElementSibling;
792 if (isVisibleEditorAnchor(prev)) {
796 prev = prev.previousElementSibling;
799 let parent = hiddenInput.parentElement;
801 while (parent && parent !== document.body) {
802 if (parent.matches('form, .dbxForm_wrapper')) {
803 parent = parent.parentElement;
807 if (isVisibleEditorAnchor(parent)) {
811 parent = parent.parentElement;
817 function isNonPositionableElement(node) {
819 if (!node || node.nodeType !== 1) return false;
821 if (isHiddenInput(node)) return true;
823 const tagName = node.tagName.toLowerCase();
824 const type = (node.getAttribute('type') || '').toLowerCase();
826 if (tagName === 'input' && type === 'hidden') return true;
827 if (node.hidden) return true;
829 const rect = node.getBoundingClientRect ? node.getBoundingClientRect() : null;
830 if (rect && rect.width === 0 && rect.height === 0) return true;
832 const style = window.getComputedStyle(node);
833 return style.display === 'none' || style.visibility === 'hidden';
836 function findVisibleEditorAnchor(node) {
839 node.closest('.dbx-window-body'),
840 node.closest('#dbxContent'),
844 for (const candidate of candidates) {
845 if (isVisibleEditorAnchor(candidate)) {
850 return document.body;
853 function isVisibleEditorAnchor(node) {
855 if (!node || node.nodeType !== 1 || !node.getBoundingClientRect) return false;
857 const rect = node.getBoundingClientRect();
858 if (rect.width <= 0 || rect.height <= 0) return false;
860 const style = window.getComputedStyle(node);
861 return style.display !== 'none' && style.visibility !== 'hidden';
864 function parseMarker(txt) {
866 if (txt.startsWith('DBX-TPL-START|')) {
869 path: txt.split('|')[1] || ''
873 if (txt.startsWith('DBX-EDITOR|')) {
874 const parts = txt.split('|');
876 kind: (parts[1] || 'file').toLowerCase(),
877 path: parts.slice(2).join('|') || ''
886 // ==================================================
888 // ==================================================
890 function collectEditorFiles() {
892 let hasAllResourcesMode = getDbxEditMode() === 9;
894 document.querySelectorAll('.dbx-editor-files-data').forEach(node => {
895 if (node.__dbxEditorFilesRead) return;
896 node.__dbxEditorFilesRead = true;
901 payload = JSON.parse(node.textContent || '{}');
903 warn('invalid editor files payload', e);
907 if (parseInt(payload.mode, 10) === 9) {
908 hasAllResourcesMode = true;
911 const files = Array.isArray(payload.files) ? payload.files : [];
913 files.forEach(file => {
914 if (!file || !file.kind || !file.file) return;
915 const key = file.kind + '|' + file.file;
916 editorMenuFiles.set(key, {
923 if (hasAllResourcesMode) {
924 collectLoadedCssFiles();
925 collectLoadedJsFiles();
929 function collectLoadedCssFiles() {
931 const hrefs = new Set();
933 Array.from(document.styleSheets || []).forEach(sheet => {
934 if (sheet && sheet.href) {
935 hrefs.add(sheet.href);
939 document.querySelectorAll('link[rel~="stylesheet"][href]').forEach(link => {
940 hrefs.add(link.href);
943 hrefs.forEach(href => {
944 const path = stylesheetHrefToEditorPath(href);
947 editorMenuFiles.set('css|' + path, {
954 function stylesheetHrefToEditorPath(href) {
956 if (!href || href.indexOf('data:') === 0 || href.indexOf('blob:') === 0) {
963 url = new URL(href, window.location.href);
968 if (url.origin !== window.location.origin) {
972 let path = url.pathname.replace(/\\/g, '/').replace(/^\/+/, '');
973 const appRoot = window.location.pathname.replace(/\\/g, '/').split('/').filter(Boolean)[0] || '';
975 if (appRoot && path.indexOf(appRoot + '/') === 0) {
976 path = path.substring(appRoot.length + 1);
979 if (!/\.css$/i.test(path)) {
986 function collectLoadedJsFiles() {
988 document.querySelectorAll('script[src]').forEach(script => {
989 const path = assetHrefToEditorPath(script.src, 'js');
992 editorMenuFiles.set('js|' + path, {
999 function assetHrefToEditorPath(href, ext) {
1001 if (!href || href.indexOf('data:') === 0 || href.indexOf('blob:') === 0) {
1008 url = new URL(href, window.location.href);
1013 if (url.origin !== window.location.origin) {
1017 let path = url.pathname.replace(/\\/g, '/').replace(/^\/+/, '');
1018 const appRoot = window.location.pathname.replace(/\\/g, '/').split('/').filter(Boolean)[0] || '';
1020 if (appRoot && path.indexOf(appRoot + '/') === 0) {
1021 path = path.substring(appRoot.length + 1);
1024 if (!new RegExp('\\.' + ext + '$', 'i').test(path)) {
1031 function getDbxEditMode() {
1033 const value = new URLSearchParams(window.location.search).get('dbx_edit');
1034 return parseInt(value || '0', 10) || 0;
1037 function getEditorFileUrl(file) {
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 || '/');
1043 if (kind === 'dd') {
1044 const match = path.match(/^dbx\/modules\/([^/]+)\/dd\/([^/]+)\.dd\.php$/);
1046 return requestPath + '?dbx_modul=dbxAdmin&dbx_run1=edit_dd&modul=' +
1047 encodeURIComponent(match[1]) +
1048 '&dd=' + encodeURIComponent(match[2]);
1052 if (kind === 'fd') {
1053 const match = path.match(/^dbx\/modules\/([^/]+)\/fd\/([^/]+)\.fd\.php$/);
1055 return requestPath + '?dbx_modul=dbxAdmin&dbx_run1=edit_fd&modul=' +
1056 encodeURIComponent(match[1]) +
1057 '&fd=' + encodeURIComponent(match[2]);
1061 return requestPath + '?dbx_modul=dbxEditor&dbx_run1=edit&file=' + encodeURIComponent(path);
1064 function watchEditorFilesData() {
1066 if (window.__dbxEditorFilesObserver) return;
1068 const observer = new MutationObserver(mutations => {
1069 if (window.__dbxEditorFilesRendering) return;
1071 const hasExternalEditorDataChange = mutations.some(mutation => {
1072 const target = mutation.target;
1074 if (target && target.closest && target.closest('#dbxEditorFilesMenu')) {
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;
1088 if (!hasExternalEditorDataChange) return;
1090 collectEditorFiles();
1091 renderEditorFilesMenu();
1094 observer.observe(document.body, {
1099 window.__dbxEditorFilesObserver = observer;
1102 function renderEditorFilesMenu() {
1104 let menu = document.getElementById('dbxEditorFilesMenu');
1106 if (!editorMenuFiles.size) {
1109 syncEditorFilesMenuLayer(null);
1114 if (menu && menu.tagName.toLowerCase() !== 'li') {
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';
1124 if (!attachEditorMenu(menu)) {
1128 const files = sortEditorFiles(Array.from(editorMenuFiles.values()));
1129 const signature = JSON.stringify(files);
1131 if (menu.dataset.dbxEditorFilesSignature === signature) {
1137 files.forEach(file => {
1138 const kind = file.kind || 'file';
1139 grouped[kind] = grouped[kind] || [];
1140 grouped[kind].push(file);
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>';
1146 html += '<ul class="dbx-menu-list dbx-editor-files-list">';
1148 getEditorKindOrder(grouped).forEach(kind => {
1149 html += '<li class="dbx-menu-item dbx-editor-files-title">' + escapeHtml(getMarkerTitle({ kind })) + '</li>';
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>';
1163 window.__dbxEditorFilesRendering = true;
1166 menu.dataset.dbxEditorFilesSignature = signature;
1167 menu.innerHTML = html;
1169 bindEditorMenu(menu);
1173 window.__dbxEditorFilesRendering = false;
1177 function attachEditorMenu(menu) {
1179 const slot = getEditorMenuSlot();
1182 const parent = slot.parentElement;
1183 const spacer = parent && parent.querySelector(':scope > .dbx-menu-spacer');
1185 if (menu.parentElement !== parent) {
1186 slot.insertAdjacentElement('afterend', menu);
1187 } else if (menu.previousElementSibling !== slot) {
1188 slot.insertAdjacentElement('afterend', menu);
1191 if (spacer && (spacer.compareDocumentPosition(menu) & Node.DOCUMENT_POSITION_PRECEDING)) {
1192 slot.insertAdjacentElement('afterend', menu);
1195 menu.classList.add('dbx-menu-right');
1196 menu.style.display = '';
1200 if (menu.parentElement) {
1207 function getEditorMenuSlot() {
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'));
1211 if (!slots.length) return null;
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;
1220 function bindEditorMenu(menu) {
1222 const toggle = menu.querySelector('.dbx-editor-files-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');
1230 if (!menu.__dbxEditorFilesLayerObserver && window.MutationObserver) {
1231 menu.__dbxEditorFilesLayerObserver = new MutationObserver(() => syncEditorFilesMenuLayer(menu));
1232 menu.__dbxEditorFilesLayerObserver.observe(menu, {
1234 attributeFilter: ['class']
1238 syncEditorFilesMenuLayer(menu);
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);
1249 window.__dbxEditorMenuCloseBound = true;
1252 menu.onclick = event => {
1253 const link = event.target.closest('a');
1255 if (link.classList.contains('dbx-editor-files-toggle')) return;
1256 menu.classList.remove('is-open');
1257 syncEditorFilesMenuLayer(menu);
1261 function syncEditorFilesMenuLayer(menu) {
1263 const open = !!(menu && menu.classList.contains('is-open'));
1264 const body = document.body;
1265 const header = document.getElementById('dbxHeader');
1268 body.classList.toggle('dbx-editor-files-menu-open', open);
1272 header.classList.toggle('dbx-editor-files-menu-open', open);
1276 const toggle = menu.querySelector('.dbx-editor-files-toggle');
1278 toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
1283 function sortEditorFiles(files) {
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;
1304 if (rankA !== rankB) return rankA - rankB;
1306 return getShortPath(a.path || '').localeCompare(getShortPath(b.path || ''), undefined, {
1312 function getEditorKindOrder(grouped) {
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]);
1318 existing.forEach(kind => {
1319 if (!ordered.includes(kind)) {
1327 function getShortPath(path) {
1329 const parts = String(path || '').split('/');
1330 const normalized = parts.join('/');
1331 const prefix = 'dbx/modules/';
1333 if (normalized.indexOf(prefix) === 0) {
1334 return normalized.substring(prefix.length);
1337 if (normalized.indexOf('dbx/design/') === 0) {
1338 return normalized.substring('dbx/'.length);
1344 function escapeHtml(value) {
1346 const div = document.createElement('div');
1347 div.textContent = String(value || '');
1348 return div.innerHTML;
1351 function encodeDataValue(value) {
1353 return encodeURIComponent(String(value || ''));
1356 function getMarkerIcon(meta) {
1358 return getFileIcon(meta.path || '');
1361 function getMarkerTitle(meta) {
1363 const kind = (meta.kind || 'tpl').toLowerCase();
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';
1375 return meta.path ? getGenericFileTitle(meta.path) : 'Datei';
1378 function getTemplateOverlayTitle(path) {
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');
1385 const modul = modulesIndex !== -1 && parts[modulesIndex + 1]
1386 ? parts[modulesIndex + 1]
1387 : (tplIndex > 0 ? parts[tplIndex - 1] : 'dbx');
1389 let templateName = 'Template';
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];
1397 return modul + '|' + templateName;
1400 function getTemplateTitle(path) {
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');
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(' - ');
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(' - ');
1422 function getDesignTitle(path) {
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]
1430 const type = designIndex !== -1 && parts[designIndex + 2]
1431 ? parts[designIndex + 2]
1433 const base = parts.length ? parts[parts.length - 1] : normalized;
1434 const parsed = splitFileName(base);
1436 return ['Design Page', design, parsed.name, (parsed.ext || type)].filter(Boolean).join(' - ');
1439 function getCssTitle(path) {
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);
1446 return ['CSS', parsed.name, parsed.ext].filter(Boolean).join(' - ');
1449 function getJsTitle(path) {
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);
1456 return ['JS', parsed.name, parsed.ext].filter(Boolean).join(' - ');
1459 function getConfigTitle(path) {
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);
1468 const modul = modulesIndex !== -1 && parts[modulesIndex + 1]
1469 ? parts[modulesIndex + 1]
1470 : (cfgIndex > 0 ? parts[cfgIndex - 1] : '');
1472 const name = parsed.name === 'config' ? 'config' : (parsed.name || 'config');
1473 return [modul, name, parsed.ext].filter(Boolean).join(' - ');
1476 function getGenericFileTitle(path) {
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);
1483 return [parsed.name || 'Datei', parsed.ext].filter(Boolean).join(' - ');
1486 function splitFileName(fileName) {
1488 const base = String(fileName || '');
1489 const dot = base.lastIndexOf('.');
1491 if (dot > 0 && dot < base.length - 1) {
1493 name: base.substring(0, dot),
1494 ext: base.substring(dot + 1)
1504 function getMarkerOffset(kind) {
1506 kind = (kind || 'tpl').toLowerCase();
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;
1517 function getFileIcon(path) {
1519 const ext = (path.split('.').pop() || '').toLowerCase();
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';
1526 return 'bi-file-earmark';