2 * openWin.js - Fenster-Manager Bibliothek
4 * Core-unabhängiges CSS-Loading
7(function (window, document, $) {
10 if (!window.dbx || !$) {
11 console.warn("openWin: dbx oder jQuery nicht verfügbar");
15 const dbx = window.dbx;
18 // --------------------------------------------------
20 // --------------------------------------------------
23 // Z-Index Basiswerte: normale openWin-Fenster bleiben hoch,
24 // echte Werkzeug-/Optionsdialoge bekommen bei Bedarf die Top-Ebene.
25 BASE_Z_DRAGGABLE: 70000,
29 ANIMATION_DURATION: 200,
35 RESIZE_HANDLE_SIZE: 12,
37 DEFAULT_POPUP_WIDTH: 800,
38 DEFAULT_POPUP_HEIGHT: 600,
40 STATE_KEY: "dbx.openWin.state",
44 // --------------------------------------------------
45 // WINDOW MANAGER KLASSE
46 // --------------------------------------------------
51 this.windows = new Map();
53 this.activeDrag = null;
54 this.activeResize = null;
56 this.cssLoaded = false;
57 this.stateSaveTimer = null;
58 this.restoringState = false;
59 this.stateRestored = false;
61 this.initEventSystem();
62 this.initGlobalDelegation();
66 // --------------------------------------------------
67 // EVENT SYSTEM (ZENTRAL)
68 // --------------------------------------------------
71 $(document).on('mousemove.openwin', (e) => this.handleMouseMove(e));
72 $(document).on('mouseup.openwin', (e) => this.handleMouseUp(e));
73 $(document).on('touchmove.openwin', (e) => this.handleMouseMove(e));
74 $(document).on('touchend.openwin touchcancel.openwin', (e) => this.handleMouseUp(e));
75 $(document).on('keydown.openwin', (e) => this.handleKeyDown(e));
76 $(window).on('beforeunload.openWinState', () => {
81 initGlobalDelegation() {
82 dbx.on("click", ".dbx-win, [data-dbx*='lib=openWin']", (e, el) => {
83 this.handleWinClick(e, el);
86 // 🔥 NEU: Window Actions
87 dbx.on("click", ".dbx-window [data-action]", (e, el) => {
88 this.handleWindowAction(e, el);
91 $(document).on("click.openWinDock", "#windrop .dbx-window-drop", (e) => {
93 const windowId = $(e.currentTarget).attr("data-window-id");
94 this.restoreWindow(windowId);
97 $(document).on("click.openWinCloseAll", "#dbxWindowCloseAll", (e) => {
99 this.closeAllWindows();
103 handleWindowAction(e, el) {
104 const action = el.dataset.action;
107 const $win = $(el).closest(".dbx-window");
108 if (!$win.length) return;
110 const windowId = $win.attr("id");
111 const windowData = this.windows.get(windowId);
112 if (!windowData) return;
114 this.safeLog("action", action, windowId);
119 this.closeWindow(windowId);
123 this.reloadWindow(windowId);
127 $win.find(".dbx-window-maximize").trigger("click");
131 if (!this.isMobileViewport()) {
132 $win.find(".dbx-window-minimize").trigger("click");
137 this.bringToFront(windowId);
142 let value = el.dataset.value;
143 let label = el.dataset.label;
145 if (value === undefined) {
146 value = $(el).text().trim();
149 if (label === undefined) {
153 const item = { value, label };
156 if (windowData.cfg.multi == 1) {
158 const idx = windowData.selectedValues.findIndex(v => v.value == value);
161 windowData.selectedValues.splice(idx, 1);
162 $(el).removeClass("dbx-selected");
164 windowData.selectedValues.push(item);
165 $(el).addClass("dbx-selected");
168 // 🔥 keepOpen entscheidet ob direkt resolved wird
169 if (windowData.cfg.keepOpen != 1) {
170 this.resolve(windowId, windowData.selectedValues);
174 this.resolve(windowId, item);
185 if (!el) return null;
187 if (dbx.declare && dbx.declare.schemas && dbx.declare.schemas.openWin) {
188 const list = dbx.declare.resolve("openWin", el);
189 if (list && list.length) return list[0];
192 const raw = el.getAttribute("data-dbx");
193 if (!raw) return null;
195 const cfgList = dbx.parseData(raw);
196 return cfgList.find(c => c.lib == "openWin") || null;
199 handleWinClick(e, el) {
201 const cfg = this.readWinConfig(el);
206 if (el.__dbxOpening) return;
207 el.__dbxOpening = true;
208 setTimeout(() => el.__dbxOpening = false, 300);
210 this.openWindow(el, cfg);
213 // --------------------------------------------------
214 // MOUSE EVENT HANDLER
215 // --------------------------------------------------
218 if (this.activeDrag) {
219 this.updateDragPosition(e);
221 if (this.activeResize) {
222 this.updateResizeSize(e);
228 // 🔥 NEU: dragging / resizing cleanup
229 const changedWindowId = this.activeDrag?.windowId || this.activeResize?.windowId || null;
231 if (this.activeDrag) {
232 const w = this.windows.get(this.activeDrag.windowId);
233 if (w) w.element.removeClass("dbx-window-dragging");
236 if (this.activeResize) {
237 const w = this.windows.get(this.activeResize.windowId);
238 if (w) w.element.removeClass("dbx-window-resizing");
241 this.activeDrag = null;
242 this.activeResize = null;
244 if (changedWindowId) {
245 this.scheduleSaveState();
251 // 🔥 PROMPT KEYBOARD + TYPEAHEAD
252 if (this.stack.length > 0) {
254 const activeWin = this.stack[this.stack.length - 1];
255 if (activeWin && activeWin.cfg.mode === "prompt") {
257 const $win = activeWin.element;
258 const $allItems = $win.find("[data-action='select']"); // 🔥 FIX
259 const $items = $allItems.filter(":visible"); // 🔥 FIX
264 const filterMode = activeWin.cfg.filter || "highlight";
266 // 🔥 INIT TYPE BUFFER
267 if (!activeWin._typeBuffer) activeWin._typeBuffer = "";
268 if (!activeWin._typeTimer) activeWin._typeTimer = null;
270 let $current = $items.filter(".dbx-active").first();
272 if (!$current.length) {
273 $items.removeClass("dbx-active"); // 🔥 FIX
274 $current = $items.first().addClass("dbx-active");
277 let idx = $items.index($current);
279 if (idx === -1) idx = 0; // 🔥 FIX
281 // --------------------------------------------------
283 // --------------------------------------------------
284 if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
286 activeWin._typeBuffer += e.key.toLowerCase();
288 clearTimeout(activeWin._typeTimer);
289 activeWin._typeTimer = setTimeout(() => {
291 activeWin._typeBuffer = "";
293 // 🔥 RESET FILTER (ALLE ITEMS!)
294 $allItems.each(function() { // 🔥 FIX
297 $el.removeClass("dbx-match");
302 const search = activeWin._typeBuffer;
306 $allItems.each(function(i) { // 🔥 FIX
309 const label = ($el.data("label") || $el.text()).toString().toLowerCase();
311 const match = label.includes(search);
314 if (filterMode === "hide") {
320 if (filterMode === "highlight") {
321 $el.toggleClass("dbx-match", match);
323 $el.removeClass("dbx-match");
326 if (match && foundIndex === -1) {
331 const $visible = $allItems.filter(":visible"); // 🔥 FIX
333 if (foundIndex !== -1 && $visible.length) {
335 $allItems.removeClass("dbx-active"); // 🔥 FIX
337 const $target = $visible.first(); // 🔥 FIX (stabil)
339 $target.addClass("dbx-active");
341 const el = $target[0];
342 if (el && el.scrollIntoView) {
343 el.scrollIntoView({ block: "nearest" });
351 // --------------------------------------------------
353 // --------------------------------------------------
354 if (e.key === "ArrowDown") {
355 idx = Math.min(idx + 1, $items.length - 1);
356 $items.removeClass("dbx-active"); // 🔥 FIX
357 $items.eq(idx).addClass("dbx-active");
362 if (e.key === "ArrowUp") {
363 idx = Math.max(idx - 1, 0);
364 $items.removeClass("dbx-active"); // 🔥 FIX
365 $items.eq(idx).addClass("dbx-active");
370 // --------------------------------------------------
372 // --------------------------------------------------
373 if (e.key === "Enter") {
374 $items.eq(idx).trigger("click");
382 // --------------------------------------------------
383 // BESTEHENDES VERHALTEN (UNVERÄNDERT)
384 // --------------------------------------------------
386 if (e.key == 'Escape' && this.stack.length > 0) {
388 const activeWin = this.stack[this.stack.length - 1];
389 if (!activeWin) return;
391 if (activeWin.fullscreen && !this.isMobileViewport()) {
393 const $win = activeWin.element;
395 if (activeWin.originalSize) {
397 width: activeWin.originalSize.width,
398 height: activeWin.originalSize.height,
399 left: activeWin.originalPosition.left,
400 top: activeWin.originalPosition.top
404 activeWin.fullscreen = false;
405 $win.removeClass("dbx-window-fullscreen");
407 $win.find(".dbx-window-maximize")
408 .html('<i class="bi bi-square"></i>');
414 if (activeWin.cfg.closable != 0) {
415 this.closeWindow(activeWin.id);
420 const activeElement = document.activeElement;
421 if (activeElement && activeElement.closest) {
422 if (this.isKeyboardEditingElement(activeElement)) return;
423 const windowEl = activeElement.closest('.dbx-window');
425 this.moveWindowWithKeys(windowEl.id, e);
430 isKeyboardEditingElement(el) {
431 if (!el || !el.closest) return false;
432 const target = el.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""], [role="textbox"], .jodit-ui-input, .jodit-ui-text_area');
433 if (!target) return false;
434 if (target.matches && target.matches('input')) {
435 const type = String(target.getAttribute('type') || 'text').toLowerCase();
436 return !['button', 'checkbox', 'color', 'file', 'image', 'radio', 'range', 'reset', 'submit'].includes(type);
441 moveWindowWithKeys(windowId, e) {
442 const windowData = this.windows.get(windowId);
443 if (!windowData || windowData.cfg.draggable == 0) return;
444 if (this.isMobileViewport()) return;
446 const step = e.shiftKey ? 10 : 1;
447 const currentPos = windowData.element.position();
448 let newLeft = currentPos.left;
449 let newTop = currentPos.top;
452 case 'ArrowLeft': newLeft -= step; break;
453 case 'ArrowRight': newLeft += step; break;
454 case 'ArrowUp': newTop -= step; break;
455 case 'ArrowDown': newTop += step; break;
459 const maxLeft = window.innerWidth - windowData.element.outerWidth();
460 const maxTop = window.innerHeight - windowData.element.outerHeight();
462 newLeft = Math.min(Math.max(newLeft, 0), maxLeft);
463 newTop = Math.min(Math.max(newTop, 0), maxTop);
465 windowData.element.css({ left: newLeft + 'px', top: newTop + 'px' });
469 // --------------------------------------------------
470 // Z-INDEX MANAGEMENT
471 // --------------------------------------------------
473 isTopZWindow(windowDataOrCfg) {
474 const cfg = windowDataOrCfg && windowDataOrCfg.cfg ? windowDataOrCfg.cfg : (windowDataOrCfg || {});
475 const priority = String(cfg.priority || cfg.zPriority || "").toLowerCase();
476 return cfg.topZ == 1 || cfg.modalTop == 1 || priority === "top" || priority === "modal-top";
479 windowBaseZ(windowDataOrCfg, isModal = false) {
480 const cfg = windowDataOrCfg && windowDataOrCfg.cfg ? windowDataOrCfg.cfg : (windowDataOrCfg || {});
481 if (this.isTopZWindow(windowDataOrCfg)) return CONFIG.BASE_Z_TOP;
482 return (isModal || cfg.modal == 1 || cfg.mode == "modal-fullscreen") ? CONFIG.BASE_Z_MODAL : CONFIG.BASE_Z_DRAGGABLE;
486 if (!el) return null;
487 if (el.jquery && el[0]) return el[0];
488 if (el.nodeType === 1) return el;
493 el = this.domElement(el);
495 const z = parseInt(window.getComputedStyle(el).zIndex, 10);
496 return Number.isFinite(z) ? z : 0;
499 maxElementZIndex(el) {
501 let cur = this.domElement(el);
502 while (cur && cur !== document.documentElement) {
503 max = Math.max(max, this.numericZIndex(cur));
504 cur = cur.parentElement;
509 callerZIndexFloor(callerEl) {
510 return this.maxElementZIndex(callerEl);
513 nextZAboveCaller(zIndex, callerEl) {
514 const callerZ = this.callerZIndexFloor(callerEl);
516 zIndex = Math.max(zIndex, callerZ + CONFIG.Z_INCREMENT);
518 return Math.min(2147483647, zIndex);
521 getNextZIndex(isModal = false, cfg = null, callerEl = null) {
522 const baseZ = this.windowBaseZ(cfg || {}, isModal);
523 return this.nextZAboveCaller(baseZ + (this.stack.length * CONFIG.Z_INCREMENT), callerEl);
526 bringToFront(windowId) {
527 const windowData = this.windows.get(windowId);
528 if (!windowData) return;
530 const idx = this.stack.findIndex(w => w.id == windowId);
532 const [win] = this.stack.splice(idx, 1);
533 this.stack.push(win);
536 this.windows.forEach(w => {
537 w.element.removeClass("dbx-window-active");
540 windowData.element.addClass("dbx-window-active");
541 this.updateAllZIndices();
543 const newZIndex = parseInt(windowData.element.css('z-index'), 10) || 0;
544 this.safeLog("bringToFront", windowId, "z-index:", newZIndex);
545 this.scheduleSaveState();
548 activateTopVisibleWindow(excludeWindowId = null) {
549 for (let i = this.stack.length - 1; i >= 0; i--) {
550 const windowData = this.stack[i];
551 if (!windowData || windowData.id == excludeWindowId) continue;
552 if (windowData.closing || windowData.minimized) continue;
553 if (!windowData.element || !windowData.element.is(":visible")) continue;
555 this.bringToFront(windowData.id);
559 this.windows.forEach(w => {
560 w.element.removeClass("dbx-window-active");
562 this.updateAllZIndices();
565 focusWindow(windowId) {
566 const windowData = this.windows.get(windowId);
567 if (!windowData) return;
569 const $win = windowData.element;
570 const $active = $win.find("input,textarea,select,button,a,[tabindex]").filter(":visible").first();
572 if ($active.length) {
579 restoreWindow(windowId) {
580 const windowData = this.windows.get(windowId);
581 if (!windowData) return;
583 if (this.isMobileViewport()) {
584 this.applyMobileWindowMode(windowData);
585 this.bringToFront(windowId);
586 this.focusWindow(windowId);
590 const $win = windowData.element;
592 if (windowData.minimized || !$win.is(":visible")) {
593 const $dockItem = this.getDockItem(windowId);
594 const fromRect = this.getElementRect($dockItem);
596 if (windowData.maximized && windowData.originalSize && windowData.originalPosition) {
598 width: windowData.originalSize.width,
599 height: windowData.originalSize.height,
600 left: windowData.originalPosition.left,
601 top: windowData.originalPosition.top
603 windowData.maximized = false;
604 $win.find(".dbx-window-maximize").html('<i class="bi bi-square"></i>');
607 if (windowData.overlay) {
608 windowData.overlay.show();
611 $win.stop(true, true).css("display", "flex");
612 $win.css("visibility", "hidden");
613 const toRect = this.getElementRect($win);
615 windowData.minimized = false;
616 $win.removeClass("dbx-window-is-minimized");
617 $win.find(".dbx-window-minimize").html('<i class="bi bi-dash"></i>');
619 this.animateDockTransfer($win, fromRect, toRect, windowData.cfg, "restore", () => {
620 this.removeDockItem(windowId);
621 this.focusWindow(windowId);
622 this.scheduleSaveState();
626 this.bringToFront(windowId);
627 this.focusWindow(windowId);
631 let $dock = $("#windrop");
634 $dock = $('<div id="windrop"></div>');
636 const $footer = $("#dbxFooter");
637 if ($footer.length) {
638 $footer.prepend($dock);
640 $("body").append($dock.addClass("dbx-window-dock-floating"));
644 if (!$dock.attr("aria-label")) {
645 $dock.attr("aria-label", "Minimierte Fenster");
648 return $dock.addClass("dbx-window-dock");
652 const $footer = $("#dbxFooter");
653 if (!$footer.length) return;
655 const windowCount = this.windows.size;
656 const dockCount = $("#windrop .dbx-window-drop").length;
657 const $closeAll = $("#dbxWindowCloseAll");
660 .toggleClass("has-open-windows", windowCount > 0)
661 .toggleClass("has-docked-windows", dockCount > 0);
663 if ($closeAll.length) {
665 .prop("disabled", windowCount < 1)
666 .attr("title", windowCount > 0 ? "Alle Fenster schließen (" + windowCount + ")" : "Keine Fenster geöffnet")
667 .attr("aria-hidden", windowCount > 0 ? "false" : "true");
671 getDockItem(windowId) {
672 if (!windowId) return $();
674 return this.getDock()
675 .children(".dbx-window-drop")
676 .filter((idx, el) => el.getAttribute("data-window-id") == windowId)
680 createDockItem(windowData) {
681 const title = this.getDockTitle(windowData);
682 let $item = this.getDockItem(windowData.id);
686 <button type="button" class="dbx-window-drop">
687 <i class="bi bi-window"></i>
688 <span class="dbx-window-drop-title"></span>
692 this.getDock().append($item);
696 .attr("data-window-id", windowData.id)
697 .attr("title", title)
698 .attr("aria-label", "Fenster öffnen: " + title)
699 .removeClass("is-restoring")
700 .addClass("is-minimized");
702 $item.find(".dbx-window-drop-title").text(title);
708 getDockTitle(windowData) {
709 if (!windowData) return "Fenster";
711 const cfgTitle = windowData.cfg && windowData.cfg.title;
712 const domTitle = windowData.element ? windowData.element.find(".dbx-window-title").text() : "";
713 return (cfgTitle || domTitle || "Fenster").toString();
716 removeDockItem(windowId) {
717 this.getDockItem(windowId).remove();
721 getElementRect($el) {
722 if (!$el || !$el.length || !$el[0].getBoundingClientRect) return null;
724 const rect = $el[0].getBoundingClientRect();
725 if (!rect.width || !rect.height) return null;
735 getDockAnimationDuration(cfg) {
738 const duration = parseInt(
739 cfg.dockAnimDuration ||
740 cfg.dockAnimationDuration ||
741 cfg.windropAnimDuration ||
742 cfg.windropAnimationDuration,
746 if (!isNaN(duration)) return Math.max(320, duration);
748 return Math.max(680, this.getAnimationDuration(cfg));
751 animateDockTransfer($win, fromRect, toRect, cfg, mode, done) {
752 const duration = this.getDockAnimationDuration(cfg);
754 if (!$win || !$win.length || !fromRect || !toRect || duration <= 0 || !window.requestAnimationFrame) {
755 if ($win && $win.length) {
756 $win.css("visibility", "");
762 const startRect = mode == "restore" ? toRect : fromRect;
763 const endRect = mode == "restore" ? fromRect : toRect;
764 const scaleX = Math.max(0.04, endRect.width / startRect.width);
765 const scaleY = Math.max(0.04, endRect.height / startRect.height);
766 const translateX = endRect.left - startRect.left;
767 const translateY = endRect.top - startRect.top;
768 const startTransform = mode == "restore"
769 ? `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`
770 : "translate(0, 0) scale(1, 1)";
771 const endTransform = mode == "restore"
772 ? "translate(0, 0) scale(1, 1)"
773 : `translate(${translateX}px, ${translateY}px) scale(${scaleX}, ${scaleY})`;
775 const originalTransition = el.style.transition;
776 const originalTransform = el.style.transform;
777 const originalTransformOrigin = el.style.transformOrigin;
778 const originalPointerEvents = el.style.pointerEvents;
779 let finished = false;
781 const cleanup = () => {
782 $win.removeClass("dbx-window-docking");
783 el.style.transition = originalTransition;
784 el.style.transform = originalTransform;
785 el.style.transformOrigin = originalTransformOrigin;
786 el.style.pointerEvents = originalPointerEvents;
789 const finish = () => {
790 if (finished) return;
793 $win.off("transitionend.openWinDock");
795 if (mode == "minimize") {
806 .addClass("dbx-window-docking")
810 transformOrigin: "left top",
811 transform: startTransform,
812 pointerEvents: "none"
817 window.requestAnimationFrame(() => {
818 window.requestAnimationFrame(() => {
820 transition: `transform ${duration}ms cubic-bezier(0.18, 0.86, 0.24, 1)`,
821 transform: endTransform
826 const timer = setTimeout(finish, duration + 80);
827 $win.on("transitionend.openWinDock", (e) => {
828 const event = e.originalEvent || e;
829 if (e.target !== el || event.propertyName !== "transform") return;
834 updateAllZIndices() {
835 this.stack.forEach((windowData, idx) => {
836 const baseZ = this.windowBaseZ(windowData, windowData.isModal);
838 const zIndex = baseZ + (idx * CONFIG.Z_INCREMENT);
839 windowData.element.css('z-index', zIndex);
840 if (windowData.overlay) {
841 windowData.overlay.css('z-index', zIndex - 1);
845 for (let pass = 0; pass < 2; pass++) {
846 this.stack.forEach((windowData) => {
847 const currentZ = parseInt(windowData.element.css('z-index'), 10) || 0;
848 const zIndex = this.nextZAboveCaller(currentZ, windowData.callerEl);
849 if (zIndex === currentZ) return;
850 windowData.element.css('z-index', zIndex);
851 if (windowData.overlay) {
852 windowData.overlay.css('z-index', zIndex - 1);
858 reloadWindow(windowId) {
859 const windowData = this.windows.get(windowId);
860 if (!windowData) return;
862 let url = windowData.url;
865 // 🔥 cache bust (optional, aber sinnvoll)
866 const sep = url.includes("?") ? "&" : "?";
867 url = url + sep + "_=" + Date.now();
869 this.safeLog("reload", windowId, url);
871 this.loadContent(windowId, url, windowData.cfg);
874 // --------------------------------------------------
876 // --------------------------------------------------
879 if (dbx.log && typeof dbx.log == 'function') {
880 dbx.log("openWin →", ...args);
881 } else if (console && console.log) {
882 console.log("[openWin]", ...args);
887 if (dbx.warn && typeof dbx.warn == 'function') {
888 dbx.warn("openWin →", ...args);
889 } else if (console && console.warn) {
890 console.warn("[openWin]", ...args);
894 parseSize(value, base, defaultValue = null) {
895 if (value == undefined || value == null) return defaultValue;
897 const strValue = value.toString().trim().toLowerCase();
899 if (strValue == "auto") return "auto";
901 if (strValue.endsWith("%")) {
902 const p = parseFloat(strValue);
903 if (!isNaN(p)) return Math.round(base * p / 100);
906 let numericValue = parseInt(strValue);
907 if (strValue.endsWith("px")) {
908 numericValue = parseInt(strValue);
911 if (!isNaN(numericValue)) return numericValue;
916 optionEnabled(value, defaultValue = true) {
917 if (value == undefined || value == null || value === "") return defaultValue;
918 if (value === false) return false;
920 const strValue = value.toString().trim().toLowerCase();
921 return !["0", "false", "off", "no", "nein"].includes(strValue);
925 return value == undefined || value == null || value === "";
929 const maxWidth = 768;
931 if (window.matchMedia) {
932 return window.matchMedia("(max-width: " + maxWidth + "px)").matches;
935 return window.innerWidth <= maxWidth;
938 applyMobileWindowMode(windowData) {
939 if (!windowData || !this.isMobileViewport()) return;
941 const $win = windowData.element;
942 if (!$win || !$win.length) return;
945 const heightValue = (
948 CSS.supports("height", "100dvh")
949 ) ? "100dvh" : window.innerHeight + "px";
951 windowData.minimized = false;
952 windowData.fullscreen = true;
953 windowData.maximized = true;
955 this.removeDockItem(windowData.id);
958 .removeClass("dbx-window-is-minimized")
959 .addClass("dbx-window-fullscreen dbx-window-mobile-fullscreen")
960 .css("display", "flex");
962 el.style.setProperty("position", "fixed", "important");
963 el.style.setProperty("left", "0", "important");
964 el.style.setProperty("top", "0", "important");
965 el.style.setProperty("width", "100vw", "important");
966 el.style.setProperty("height", heightValue, "important");
967 el.style.setProperty("max-width", "100vw", "important");
968 el.style.setProperty("max-height", heightValue, "important");
969 el.style.setProperty("min-width", "0", "important");
970 el.style.setProperty("min-height", "0", "important");
972 $win.find(".dbx-window-minimize").hide();
973 $win.find(".dbx-window-maximize").hide();
974 $win.find(".dbx-window-resize").hide();
976 if (windowData.overlay) {
977 windowData.overlay.show();
981 normalizeConfig(cfg) {
984 if (this.optionUnset(cfg.scroll)) {
988 if (this.optionUnset(cfg.reloadable) && this.optionUnset(cfg.reload)) {
995 getStateStorageKey() {
996 const path = (window.location && window.location.pathname) ? window.location.pathname : "default";
997 return CONFIG.STATE_KEY + ":" + path;
1002 if (!dbx.uiGet) return null;
1004 const data = dbx.uiGet("openWin", this.getStateStorageKey(), "state", null);
1005 if (!data || data.version !== CONFIG.STATE_VERSION || !Array.isArray(data.windows)) {
1011 this.safeWarn("state read failed", e);
1018 if (!dbx.uiSet) return false;
1019 dbx.uiSet("openWin", this.getStateStorageKey(), "state", data && data.windows && data.windows.length ? data : null);
1022 this.safeWarn("state write failed", e);
1029 if (!dbx.uiSet) return false;
1030 dbx.uiSet("openWin", this.getStateStorageKey(), "state", null);
1033 this.safeWarn("state clear failed", e);
1038 getRestoreMode(options = {}) {
1039 const configMode = dbx.config && (
1040 dbx.config.openWinRestoreMode ||
1041 (dbx.config.openWin && dbx.config.openWin.restoreMode)
1043 const mode = (options.mode || options.restoreMode || configMode || "minimized").toString().toLowerCase();
1045 return mode == "minimized" || mode == "minimize" || mode == "windrop" ? "minimized" : "state";
1048 isPersistableWindow(windowData) {
1049 if (!windowData || !windowData.url) return false;
1050 if (windowData.closing) return false;
1052 const cfg = windowData.cfg || {};
1053 const mode = (cfg.mode || "panel").toString().toLowerCase();
1054 const method = (cfg.method || "GET").toString().toUpperCase();
1056 if (!this.optionEnabled(cfg.persist ?? cfg.restoreOnReload ?? cfg.saveState, true)) return false;
1057 if (cfg.content !== undefined || cfg.html !== undefined) return false;
1058 if (["modal-fullscreen", "prompt", "popup", "confirm"].includes(mode)) return false;
1059 if (method !== "GET") return false;
1064 sanitizeStateConfig(cfg) {
1066 "restore", "__restore", "__restoreState", "__restoreMinimized",
1067 "callerEl", "onBeforeClose", "onOpen", "onClose", "onLoad",
1068 "onError", "onReuse", "onMaxReached"
1072 return JSON.parse(JSON.stringify(cfg || {}, (key, value) => {
1073 if (!key) return value;
1074 if (skipKeys.includes(key) || key.startsWith("__") || /^on[A-Z]/.test(key)) return undefined;
1075 if (typeof value == "function" || typeof value == "undefined") return undefined;
1076 if (value && (value.nodeType || value === window || value === document || value.jquery)) return undefined;
1081 Object.keys(cfg || {}).forEach(key => {
1082 const value = cfg[key];
1083 if (skipKeys.includes(key) || key.startsWith("__") || /^on[A-Z]/.test(key)) return;
1084 if (typeof value == "function" || typeof value == "undefined") return;
1085 if (value && typeof value == "object") return;
1092 getWindowState(windowData, order) {
1093 if (!this.isPersistableWindow(windowData)) return null;
1095 const $win = windowData.element;
1096 const $body = $win.find(".dbx-window-body");
1099 url: windowData.url,
1100 cfg: this.sanitizeStateConfig(windowData.cfg),
1101 fingerprint: windowData.fingerprint,
1103 title: this.getDockTitle(windowData),
1105 left: $win.css("left"),
1106 top: $win.css("top"),
1107 width: $win.css("width"),
1108 height: $win.css("height"),
1109 zIndex: parseInt($win.css("z-index"), 10) || null,
1110 minimized: !!windowData.minimized,
1111 fullscreen: !!windowData.fullscreen,
1112 originalSize: windowData.originalSize || null,
1113 originalPosition: windowData.originalPosition || null,
1114 scrollTop: $body.length ? $body.scrollTop() : 0
1119 saveState(force = false) {
1120 if (this.restoringState && !force) return false;
1122 clearTimeout(this.stateSaveTimer);
1123 this.stateSaveTimer = null;
1125 const windows = this.stack
1126 .map((windowData, order) => this.getWindowState(windowData, order))
1129 return this.writeState({
1130 version: CONFIG.STATE_VERSION,
1131 savedAt: Date.now(),
1132 path: window.location ? window.location.pathname : "",
1137 scheduleSaveState() {
1138 if (this.restoringState) return;
1140 clearTimeout(this.stateSaveTimer);
1141 this.stateSaveTimer = setTimeout(() => this.saveState(), CONFIG.STATE_SAVE_DELAY);
1144 applySavedWindowState(windowData, state) {
1145 if (!windowData || !state) return;
1147 const $win = windowData.element;
1150 ["left", "top", "width", "height"].forEach(key => {
1151 if (state[key] !== undefined && state[key] !== null && state[key] !== "") {
1152 css[key] = state[key];
1156 if (Object.keys(css).length) {
1160 windowData.originalSize = state.originalSize || null;
1161 windowData.originalPosition = state.originalPosition || null;
1163 if (state.fullscreen) {
1171 windowData.fullscreen = true;
1172 $win.addClass("dbx-window-fullscreen");
1173 $win.find(".dbx-window-maximize").html('<i class="bi bi-arrows-angle-contract"></i>');
1177 restoreState(options = {}) {
1178 if (this.stateRestored && !options.force) return [];
1180 const data = this.readState();
1181 this.stateRestored = true;
1183 if (!data || !Array.isArray(data.windows) || !data.windows.length) {
1187 const mode = this.getRestoreMode(options);
1188 const restored = [];
1190 this.restoringState = true;
1194 .sort((a, b) => (a.order || 0) - (b.order || 0))
1196 if (!item || !item.url) return;
1198 const cfg = $.extend(true, {}, item.cfg || {});
1199 const isModal = (cfg.modal == 1 || cfg.mode == "modal-fullscreen");
1200 if (!this.isPersistableWindow({ url: item.url, cfg: cfg, isModal: isModal })) return;
1202 const fingerprint = this.getWindowFingerprint(cfg, item.url);
1204 if (this.findReusableWindow(fingerprint)) return;
1207 cfg.__restoreState = item.state || {};
1208 cfg.__restoreMinimized = (mode == "minimized" || (item.state && item.state.minimized)) ? 1 : 0;
1210 const windowId = this.createWindow(cfg, item.url, null);
1211 if (windowId) restored.push(windowId);
1214 this.restoringState = false;
1215 this.saveState(true);
1217 this.safeLog("state restored", restored.length, "mode:", mode);
1221 appendAjaxFlag(url) {
1222 if (!url) return url;
1223 if (url.includes("dbx_ajax=1")) return url;
1224 if (url.includes("?")) return url + "&dbx_ajax=1";
1225 return url + "?dbx_ajax=1";
1228 normalizeWindowUrl(url) {
1229 url = String(url || '').replace(/&/g, '&');
1232 const parsed = new URL(url, window.location.href);
1233 const entries = Array.from(parsed.searchParams.entries())
1234 .filter(([key]) => key !== '_')
1236 const keyCompare = a[0].localeCompare(b[0]);
1237 return keyCompare || String(a[1]).localeCompare(String(b[1]));
1241 entries.forEach(([key, value]) => parsed.searchParams.append(key, value));
1249 getWindowFingerprint(cfg, url) {
1250 const uniqueKeys = [
1251 'mode', 'modal', 'title', 'width', 'height', 'position',
1252 'left', 'top', 'sizeX', 'sizeY', 'scroll'
1255 url: this.normalizeWindowUrl(url)
1258 uniqueKeys.forEach(key => {
1259 if (cfg[key] !== undefined && cfg[key] !== null) {
1260 data[key] = cfg[key];
1264 return JSON.stringify(data);
1267 findReusableWindow(fingerprint) {
1268 if (!fingerprint) return null;
1270 for (const windowData of this.windows.values()) {
1271 if (windowData.fingerprint === fingerprint) {
1280 if (!str) return '';
1282 .replace(/&/g, '&')
1283 .replace(/</g, '<')
1284 .replace(/>/g, '>')
1285 .replace(/"/g, '"')
1286 .replace(/'/g, ''');
1289 decodeConfigText(value) {
1290 if (value === undefined || value === null) return '';
1292 const text = value.toString();
1293 if (text.indexOf('%') === -1) return text;
1296 return decodeURIComponent(text);
1302 getAnimationName(cfg, type) {
1305 if (type == "open") {
1306 return cfg.animOpen || cfg.openAnimation || cfg.animationOpen || "fade-scale-in";
1309 if (type == "minimize") {
1310 return cfg.animMin || cfg.minAnimation || cfg.animationMin || cfg.minimizeAnimation || "fade-out";
1313 if (type == "close") {
1314 return cfg.animClose || cfg.closeAnimation || cfg.animationClose || "fade-out";
1320 getAnimationDuration(cfg) {
1321 const duration = parseInt((cfg || {}).animDuration || (cfg || {}).animationDuration || CONFIG.ANIMATION_DURATION, 10);
1322 return isNaN(duration) ? CONFIG.ANIMATION_DURATION : duration;
1325 normalizeAnimationName(name) {
1326 name = (name || "none").toString().trim().toLowerCase();
1330 "fadein": "fade-in",
1331 "fade-in": "fade-in",
1332 "fadeout": "fade-out",
1333 "fade-out": "fade-out",
1334 "scale": "fade-scale-in",
1335 "fade-scale": "fade-scale-in",
1336 "fade-scale-in": "fade-scale-in",
1337 "scale-out": "fade-scale-out",
1338 "fade-scale-out": "fade-scale-out",
1339 "slide": "slide-up-in",
1340 "slide-up": "slide-up-in",
1341 "slide-up-in": "slide-up-in",
1342 "slide-down-out": "slide-down-out",
1343 "minimize": "minimize-out",
1344 "minimize-out": "minimize-out",
1349 return aliases[name] || name.replace(/[^a-z0-9_-]/g, "");
1352 animateElement($el, animationName, cfg, done) {
1353 const name = this.normalizeAnimationName(animationName);
1354 const duration = this.getAnimationDuration(cfg);
1356 if (!$el || !$el.length || name == "none" || duration <= 0) {
1361 const className = "dbx-ow-anim-" + name;
1362 let finished = false;
1364 const finish = () => {
1365 if (finished) return;
1367 clearTimeout(timer);
1368 $el.off("animationend.openWinAnimation");
1369 $el.removeClass(className + " dbx-ow-anim-running");
1370 $el[0].style.removeProperty("--dbx-openwin-animation-duration");
1374 $el.removeClass((index, classList) => {
1375 return (classList.match(/(^|\s)dbx-ow-anim-\S+/g) || []).join(" ");
1378 $el[0].style.setProperty("--dbx-openwin-animation-duration", duration + "ms");
1379 $el.addClass("dbx-ow-anim-running " + className);
1381 const timer = setTimeout(finish, duration + 80);
1382 $el.off("animationend.openWinAnimation").on("animationend.openWinAnimation", finish);
1385 triggerCallback(callback, ...args) {
1386 if (callback && typeof callback == 'function') {
1388 } else if (callback && typeof callback == 'string' && window[callback]) {
1389 window[callback](...args);
1393 triggerCallbackWithReturn(callback, ...args) {
1394 if (callback && typeof callback == 'function') {
1395 return callback(...args);
1396 } else if (callback && typeof callback == 'string' && window[callback]) {
1397 return window[callback](...args);
1402 // --------------------------------------------------
1404 // --------------------------------------------------
1406 initDrag(windowId) {
1407 const windowData = this.windows.get(windowId);
1408 if (!windowData || windowData.cfg.draggable == 0) return;
1410 if (this.isMobileViewport()) return;
1412 const $win = windowData.element;
1413 const $header = $win.find(".dbx-window-header");
1415 $header.css("cursor", "move");
1417 $header.off('mousedown.drag').on('mousedown.drag', (e) => {
1418 if (!$(e.target).closest('.dbx-window-header').length) return;
1420 if ($(e.target).closest(".dbx-window-controls, .dbx-window-close, .dbx-window-minimize, .dbx-window-maximize, .dbx-window-reload, button, input, textarea, select, a, [data-action]").length) {
1424 this.bringToFront(windowId);
1426 // 🔥 NEU: dragging class
1427 $win.addClass("dbx-window-dragging");
1433 startLeft: parseFloat($win.css('left')),
1434 startTop: parseFloat($win.css('top'))
1440 $header.off('touchstart.drag').on('touchstart.drag', (e) => {
1441 if (this.isMobileViewport()) return;
1443 if ($(e.target).closest(".dbx-window-controls, .dbx-window-close, .dbx-window-minimize, .dbx-window-maximize, .dbx-window-reload, button, input, textarea, select, a, [data-action]").length) {
1447 const touch = e.originalEvent?.touches?.[0];
1450 this.bringToFront(windowId);
1453 $win.addClass("dbx-window-dragging");
1457 startX: touch.clientX,
1458 startY: touch.clientY,
1459 startLeft: parseFloat($win.css('left')),
1460 startTop: parseFloat($win.css('top'))
1467 updateDragPosition(e) {
1468 if (!this.activeDrag) return;
1470 const windowData = this.windows.get(this.activeDrag.windowId);
1471 if (!windowData) return;
1473 const clientX = e.clientX || (e.originalEvent?.touches?.[0]?.clientX);
1474 const clientY = e.clientY || (e.originalEvent?.touches?.[0]?.clientY);
1475 if (clientX == undefined) return;
1477 const dx = clientX - this.activeDrag.startX;
1478 const dy = clientY - this.activeDrag.startY;
1480 let newLeft = this.activeDrag.startLeft + dx;
1481 let newTop = this.activeDrag.startTop + dy;
1483 const maxLeft = window.innerWidth - windowData.element.outerWidth();
1484 const maxTop = window.innerHeight - windowData.element.outerHeight();
1486 newLeft = Math.min(Math.max(newLeft, 0), maxLeft);
1487 newTop = Math.min(Math.max(newTop, 0), maxTop);
1489 windowData.element.css({
1490 left: newLeft + 'px',
1495 initResize(windowId) {
1496 const windowData = this.windows.get(windowId);
1497 if (!windowData || windowData.cfg.resizable == 0) return;
1499 if (this.isMobileViewport()) return;
1501 const $win = windowData.element;
1502 const $resize = $win.find(".dbx-window-resize");
1504 $resize.off('mousedown.resize').on('mousedown.resize', (e) => {
1505 this.bringToFront(windowId);
1508 $win.addClass("dbx-window-resizing");
1510 this.activeResize = {
1514 startWidth: $win.outerWidth(),
1515 startHeight: $win.outerHeight()
1519 e.stopPropagation();
1522 $resize.off('touchstart.resize').on('touchstart.resize', (e) => {
1523 if (this.isMobileViewport()) return;
1525 const touch = e.originalEvent?.touches?.[0];
1528 this.bringToFront(windowId);
1531 $win.addClass("dbx-window-resizing");
1533 this.activeResize = {
1535 startX: touch.clientX,
1536 startY: touch.clientY,
1537 startWidth: $win.outerWidth(),
1538 startHeight: $win.outerHeight()
1542 e.stopPropagation();
1546 updateResizeSize(e) {
1547 if (!this.activeResize) return;
1549 const windowData = this.windows.get(this.activeResize.windowId);
1550 if (!windowData) return;
1552 const clientX = e.clientX || (e.originalEvent?.touches?.[0]?.clientX);
1553 const clientY = e.clientY || (e.originalEvent?.touches?.[0]?.clientY);
1554 if (clientX == undefined) return;
1556 const dx = clientX - this.activeResize.startX;
1557 const dy = clientY - this.activeResize.startY;
1559 let newWidth = this.activeResize.startWidth + dx;
1560 let newHeight = this.activeResize.startHeight + dy;
1562 const viewportWidth = window.innerWidth;
1563 const viewportHeight = window.innerHeight;
1565 const minWidth = this.parseSize(windowData.cfg.minWidth, viewportWidth, CONFIG.MIN_WIDTH);
1566 const maxWidth = this.parseSize(windowData.cfg.maxWidth, viewportWidth, viewportWidth);
1567 const minHeight = this.parseSize(windowData.cfg.minHeight, viewportHeight, CONFIG.MIN_HEIGHT);
1568 const maxHeight = this.parseSize(windowData.cfg.maxHeight, viewportHeight, viewportHeight);
1570 newWidth = Math.min(Math.max(newWidth, minWidth), maxWidth);
1571 newHeight = Math.min(Math.max(newHeight, minHeight), maxHeight);
1573 windowData.element.css({
1574 width: newWidth + 'px',
1575 height: newHeight + 'px'
1579 // --------------------------------------------------
1580 // MINIMIZE / MAXIMIZE
1581 // --------------------------------------------------
1583 initMinimize(windowId) {
1584 const windowData = this.windows.get(windowId);
1585 if (!windowData) return;
1586 if (this.isMobileViewport()) return;
1588 const $win = windowData.element;
1589 const $minimizeBtn = $win.find(".dbx-window-minimize");
1591 if (!$minimizeBtn.length) return;
1593 $minimizeBtn.off('click.minimize').on('click.minimize', () => {
1594 if (windowData.minimized) {
1595 this.restoreWindow(windowId);
1597 this.minimizeWindow(windowId);
1602 minimizeWindow(windowId) {
1603 const windowData = this.windows.get(windowId);
1604 if (!windowData || windowData.minimized) return;
1605 if (this.isMobileViewport()) return;
1607 const $win = windowData.element;
1608 const $minimizeBtn = $win.find(".dbx-window-minimize");
1609 const $dockItem = this.createDockItem(windowData);
1610 const fromRect = this.getElementRect($win);
1611 const toRect = this.getElementRect($dockItem);
1613 this.bringToFront(windowId);
1614 $dockItem.addClass("is-minimizing");
1616 this.animateDockTransfer($win, fromRect, toRect, windowData.cfg, "minimize", () => {
1617 $win.hide().css("visibility", "").addClass("dbx-window-is-minimized");
1618 windowData.minimized = true;
1619 $win.removeClass("dbx-window-active");
1620 $minimizeBtn.html('<i class="bi bi-plus"></i>');
1621 $dockItem.removeClass("is-minimizing").addClass("is-minimized");
1623 if (windowData.overlay) {
1624 windowData.overlay.hide();
1627 this.activateTopVisibleWindow(windowId);
1628 this.scheduleSaveState();
1632 initMaximize(windowId) {
1633 const windowData = this.windows.get(windowId);
1634 if (!windowData) return;
1635 if (this.isMobileViewport()) return;
1637 const $win = windowData.element;
1638 const $maximizeBtn = $win.find(".dbx-window-maximize");
1640 if (!$maximizeBtn.length) return;
1642 $maximizeBtn.off('click.maximize').on('click.maximize', () => {
1644 // 🔥 FULLSCREEN MODE
1645 if (windowData.fullscreen) {
1647 if (windowData.originalSize) {
1649 width: windowData.originalSize.width,
1650 height: windowData.originalSize.height,
1651 left: windowData.originalPosition.left,
1652 top: windowData.originalPosition.top
1656 windowData.fullscreen = false;
1657 $win.removeClass("dbx-window-fullscreen");
1659 $maximizeBtn.html('<i class="bi bi-square"></i>');
1663 windowData.originalSize = {
1664 width: $win.css('width'),
1665 height: $win.css('height')
1668 windowData.originalPosition = {
1669 left: $win.css('left'),
1670 top: $win.css('top')
1680 windowData.fullscreen = true;
1681 $win.addClass("dbx-window-fullscreen");
1683 $maximizeBtn.html('<i class="bi bi-arrows-angle-contract"></i>');
1686 this.scheduleSaveState();
1690 // --------------------------------------------------
1692 // --------------------------------------------------
1694 loadContent(windowId, url, cfg) {
1695 const windowData = this.windows.get(windowId);
1696 if (!windowData) return;
1698 const $body = windowData.element.find(".dbx-window-body");
1699 const $target = this.getWindowInnerTarget(windowData);
1700 this.syncContentFlags(windowData);
1703 <div class="dbx-window-loading">
1708 if (!dbx.ajax || typeof dbx.ajax.request !== "function") {
1709 const errorMsg = cfg.errorMessage || `
1710 <div class='dbx-window-error text-danger p-3'>
1711 <i class="bi bi-exclamation-triangle"></i>
1712 <strong>Fehler beim Laden</strong><br>
1713 <small>ajax.js nicht geladen.</small>
1715 $target.html(errorMsg);
1716 this.triggerCallback(cfg.onError, windowData.element[0], "ajax.js nicht geladen.");
1720 const requestConfig = {
1722 method: cfg.method || "GET",
1724 data: cfg.data || {},
1725 timeout: cfg.timeout || 30000
1728 if (cfg.headers && typeof cfg.headers == 'object') {
1729 requestConfig.headers = cfg.headers;
1732 dbx.ajax.request(requestConfig)
1734 this.safeLog("loaded", url);
1735 windowData.url = url;
1736 windowData.cfg = $.extend(true, {}, windowData.cfg || {}, cfg || {});
1737 $target.html(response);
1738 this.syncContentFlags(windowData);
1740 this.rescanContent($target[0]);
1741 if (windowData.restoreState && windowData.restoreState.scrollTop) {
1742 $body.scrollTop(windowData.restoreState.scrollTop);
1744 this.triggerCallback(cfg.onLoad, windowData.element[0]);
1747 const msg = error && error.message ? error.message : "Unbekannter Fehler";
1748 this.safeWarn("load failed", url, msg);
1749 const errorMsg = cfg.errorMessage || `
1750 <div class='dbx-window-error text-danger p-3'>
1751 <i class="bi bi-exclamation-triangle"></i>
1752 <strong>Fehler beim Laden</strong><br>
1753 <small>${msg}</small>
1755 $target.html(errorMsg);
1756 this.triggerCallback(cfg.onError, windowData.element[0], error);
1760 getWindowInnerTarget(windowData) {
1761 const $body = windowData.element.find(".dbx-window-body");
1762 let $target = $body.children("[data-openwin-inner]").first();
1763 if (!$target.length) {
1764 $target = $(`<div class="dbx-window-inner" data-openwin-inner id="inner_${windowData.id}"></div>`);
1765 const existing = $body.contents().detach();
1766 $body.empty().append($target);
1767 if (existing.length) $target.append(existing);
1772 syncContentFlags(windowData) {
1773 if (!windowData || !windowData.element) return;
1774 const hasAce = windowData.element.find(".c-ace, [data-dbx*='lib=ace']").length > 0;
1775 windowData.element.toggleClass("dbx-window-has-ace", hasAce);
1778 setWindowContent(windowId, cfg) {
1779 const windowData = this.windows.get(windowId);
1780 if (!windowData) return null;
1781 cfg = this.normalizeConfig(cfg || {});
1782 const $target = this.getWindowInnerTarget(windowData);
1783 if (cfg.title !== undefined && cfg.title !== null) {
1784 const title = this.decodeConfigText(cfg.title);
1786 windowData.element.find(".dbx-window-title").html(title);
1788 if (cfg.content !== undefined) {
1789 $target.empty().append(cfg.content);
1790 } else if (cfg.html !== undefined) {
1791 $target.html(cfg.html);
1795 this.syncContentFlags(windowData);
1796 windowData.cfg = $.extend(true, {}, windowData.cfg || {}, cfg || {});
1797 this.rescanContent($target[0]);
1798 this.triggerCallback(cfg.onLoad, windowData.element[0]);
1799 this.bringToFront(windowId);
1803 replaceSelfWindowContent(el, cfg, url) {
1804 const $win = $(el).closest(".dbx-window");
1805 if (!$win.length) return null;
1806 const windowId = $win.attr("id");
1807 const windowData = this.windows.get(windowId);
1808 if (!windowData) return null;
1810 if (cfg.content !== undefined || cfg.html !== undefined) {
1811 return this.setWindowContent(windowId, cfg);
1814 if (!url) return null;
1815 if (cfg.ajax != 0) {
1816 url = this.appendAjaxFlag(url);
1818 this.loadContent(windowId, url, cfg);
1819 this.bringToFront(windowId);
1823 rescanContent(element) {
1824 // dbx.rescan existiert?
1825 if (dbx.rescan && typeof dbx.rescan == 'function') {
1826 dbx.rescan(element);
1827 } else if (dbx.scan && typeof dbx.scan == 'function') {
1831 if (dbx.iconsEditor && typeof dbx.iconsEditor.rescan == 'function') {
1832 dbx.iconsEditor.rescan(element);
1838 // --------------------------------------------------
1840 // --------------------------------------------------
1842 createOverlay(windowId) {
1843 const overlayId = `dbx_overlay_${windowId}`;
1845 const windowData = this.windows.get(windowId);
1846 if (!windowData) return;
1848 const winZ = parseInt(windowData.element.css('z-index'), 10) || CONFIG.BASE_Z_MODAL;
1849 const zIndex = winZ - 1;
1851 const $overlay = $(`
1852 <div id="${overlayId}" class="dbx-window-overlay"
1853 style="position:fixed; top:0; left:0; width:100%; height:100%;
1859 $("body").append($overlay);
1860 $overlay.fadeIn(CONFIG.ANIMATION_DURATION);
1862 if (windowData.cfg.clickToClose == 1) {
1863 $overlay.on('click', () => {
1864 this.closeWindow(windowId);
1868 windowData.overlay = $overlay;
1871 // --------------------------------------------------
1872 // FENSTER ERSTELLEN
1873 // --------------------------------------------------
1875 createWindow(cfg, url, callerEl = null) {
1876 cfg = $.extend(true, {}, cfg || {});
1877 cfg = this.normalizeConfig(cfg);
1879 const restoreState = cfg.__restoreState || null;
1880 const isRestore = cfg.__restore == 1 || cfg.restore == 1;
1881 const restoreMinimized = cfg.__restoreMinimized == 1;
1883 delete cfg.__restoreState;
1884 delete cfg.__restore;
1885 delete cfg.__restoreMinimized;
1888 if (this.stack.length >= CONFIG.MAX_WINDOWS) {
1889 this.safeWarn("maximum windows reached", CONFIG.MAX_WINDOWS);
1890 this.triggerCallback(cfg.onMaxReached);
1895 const windowId = `dbxWindow_${this.counter}_${Date.now()}`;
1897 const isModal = (cfg.modal == 1 || cfg.mode == "modal-fullscreen");
1898 const zIndex = this.getNextZIndex(isModal, cfg, callerEl);
1900 const title = this.decodeConfigText(cfg.title || 'Fenster');
1902 const footer = cfg.footer || '';
1903 const isMobileViewport = this.isMobileViewport();
1905 const showClose = cfg.closable != 0;
1906 const showReload = isMobileViewport ? false : this.optionEnabled(cfg.reloadable ?? cfg.reload, false);
1907 const showMinimize = isMobileViewport ? false : this.optionEnabled(cfg.minimizable ?? cfg.minimize, true);
1908 const showMaximize = isMobileViewport ? false : this.optionEnabled(cfg.maximizable ?? cfg.maximize, true);
1910 const scrollClass = (cfg.scroll == 0) ? 'dbx-window-no-scroll' : 'dbx-window-scroll';
1912 const customTools = isMobileViewport ? '' : (cfg.tools || '');
1915 <div class="dbx-window ${isModal ? 'dbx-window-modal' : 'dbx-window-draggable'} ${scrollClass}"
1917 style="z-index:${zIndex}; position:absolute; display:none;"
1922 <div class="dbx-window-header">
1923 <span class="dbx-window-title">${title}</span>
1925 <div class="dbx-window-controls">
1929 ${showReload ? '<span class="dbx-window-reload" data-action="reload" title="Fenster neu laden" aria-label="Fenster neu laden"><i class="bi bi-arrow-clockwise"></i></span>' : ''}
1930 ${showMinimize ? '<span class="dbx-window-minimize"><i class="bi bi-dash"></i></span>' : ''}
1931 ${showMaximize ? '<span class="dbx-window-maximize"><i class="bi bi-square"></i></span>' : ''}
1932 ${showClose ? '<span class="dbx-window-close"><i class="bi bi-x"></i></span>' : ''}
1936 <div class="dbx-window-body"><div class="dbx-window-inner" data-openwin-inner id="inner_${windowId}"></div></div>
1938 ${footer ? `<div class="dbx-window-footer">${footer}</div>` : ''}
1940 ${!isMobileViewport && cfg.resizable != 0 ? '<div class="dbx-window-resize"></div>' : ''}
1944 $("body").append(html);
1945 const $win = $(`#${windowId}`);
1947 const windowData = {
1952 fingerprint: this.getWindowFingerprint(cfg, url),
1959 originalPosition: null,
1961 restoreState: restoreState,
1965 this.windows.set(windowId, windowData);
1966 this.stack.push(windowData);
1968 this.calculateWindowSize(windowData, cfg);
1970 this.applySavedWindowState(windowData, restoreState);
1973 if (!isRestore && cfg.mode === "prompt" && callerEl && !isMobileViewport) {
1975 const rect = callerEl.getBoundingClientRect();
1976 const viewportWidth = window.innerWidth;
1977 const viewportHeight = window.innerHeight;
1979 let left = rect.left + window.scrollX;
1980 let top = rect.bottom + window.scrollY;
1982 let width = $win.outerWidth();
1983 let height = $win.outerHeight();
1986 width = Math.min(300, viewportWidth - 20);
1987 $win.css("width", width + "px");
1991 $win.css("height", "auto");
1992 height = $win.outerHeight();
1995 if (left + width > viewportWidth + window.scrollX) {
1996 left = viewportWidth + window.scrollX - width - 10;
1999 if (top + height > viewportHeight + window.scrollY) {
2000 top = rect.top + window.scrollY - height;
2003 left = Math.max(10, left);
2004 top = Math.max(10, top);
2012 // 🔥 PROMPT: OUTSIDE CLICK CLOSE
2013 if (!isRestore && cfg.mode === "prompt") {
2016 $(document).on(`mousedown.${windowId}`, (ev) => {
2018 const $target = $(ev.target);
2021 !$target.closest(`#${windowId}`).length &&
2022 (!callerEl || (!$target.is(callerEl) && !$target.closest(callerEl).length))
2025 this.closeWindow(windowId);
2035 cfg.mode == "fullscreen" ||
2036 cfg.mode == "modal-fullscreen" ||
2037 cfg.position == "fullscreen"
2047 windowData.fullscreen = true;
2048 $win.addClass("dbx-window-fullscreen");
2050 $win.find(".dbx-window-maximize")
2051 .html('<i class="bi bi-arrows-angle-contract"></i>');
2054 if (isMobileViewport) {
2055 this.applyMobileWindowMode(windowData);
2058 this.initWindowEvents(windowId, cfg);
2060 if (!isMobileViewport && cfg.draggable != 0) this.initDrag(windowId);
2061 if (!isMobileViewport && cfg.resizable != 0) this.initResize(windowId);
2062 if (!isMobileViewport && showMinimize) this.initMinimize(windowId);
2063 if (!isMobileViewport && showMaximize) this.initMaximize(windowId);
2064 if (isModal) this.createOverlay(windowId);
2066 if (cfg.content !== undefined || cfg.html !== undefined) {
2067 const $body = this.getWindowInnerTarget(windowData);
2068 if (cfg.content !== undefined) {
2069 $body.empty().append(cfg.content);
2071 $body.html(cfg.html);
2073 this.rescanContent($body[0]);
2074 this.triggerCallback(cfg.onLoad, $win[0]);
2076 this.loadContent(windowId, url, cfg);
2079 if (!isMobileViewport && restoreMinimized) {
2080 windowData.minimized = true;
2081 $win.hide().addClass("dbx-window-is-minimized");
2082 $win.find(".dbx-window-minimize").html('<i class="bi bi-plus"></i>');
2083 this.createDockItem(windowData);
2084 if (windowData.overlay) {
2085 windowData.overlay.stop(true, true).hide();
2088 $win.css("display", "flex");
2089 if (isMobileViewport) {
2090 this.applyMobileWindowMode(windowData);
2092 this.bringToFront(windowId);
2095 this.animateElement($win, this.getAnimationName(cfg, "open"), cfg);
2097 const $firstInput = $win.find("input,textarea,select").first();
2098 if ($firstInput.length) {
2099 $firstInput.focus();
2107 this.triggerCallback(cfg.onOpen, $win[0]);
2108 this.scheduleSaveState();
2111 this.safeLog("window created", windowId, "modal:", isModal, "z-index:", zIndex);
2112 this.updateDockUi();
2117 calculateWindowSize(windowData, cfg) {
2118 const $win = windowData.element;
2121 const viewportWidth = window.innerWidth;
2122 const viewportHeight = window.innerHeight;
2124 let width = this.parseSize(cfg.width, viewportWidth);
2125 let height = this.parseSize(cfg.height, viewportHeight);
2127 if (width == null || width == "auto") {
2128 width = Math.round(viewportWidth * CONFIG.DEFAULT_WIDTH);
2130 if (height == null || height == "auto") {
2131 height = Math.round(viewportHeight * CONFIG.DEFAULT_HEIGHT);
2134 const minWidth = this.parseSize(cfg.minWidth, viewportWidth, CONFIG.MIN_WIDTH);
2135 const maxWidth = this.parseSize(cfg.maxWidth, viewportWidth, viewportWidth);
2137 // 🔥 FIX: minHeight prüfen
2139 if (cfg.minHeight == undefined || cfg.minHeight == null) {
2142 minHeight = this.parseSize(cfg.minHeight, viewportHeight, CONFIG.MIN_HEIGHT);
2145 const maxHeight = this.parseSize(cfg.maxHeight, viewportHeight, viewportHeight);
2147 width = Math.min(Math.max(width, minWidth), maxWidth);
2148 height = Math.min(Math.max(height, minHeight), maxHeight);
2152 if (cfg.position == 'center-top') {
2153 left = (viewportWidth - width) / 2;
2154 top = Math.max(24, Math.round(viewportHeight * 0.04));
2155 } else if (cfg.position == 'center') {
2156 left = (viewportWidth - width) / 2;
2157 top = (viewportHeight - height) / 2;
2158 } else if (cfg.position && typeof cfg.position == 'object') {
2159 left = this.parseSize(cfg.position.x, viewportWidth, 100);
2160 top = this.parseSize(cfg.position.y, viewportHeight, 100);
2162 const offset = Math.min(this.stack.length * CONFIG.DRAG_OFFSET, 200);
2163 left = 100 + offset;
2167 left = Math.min(Math.max(left, 0), viewportWidth - 50);
2168 top = Math.min(Math.max(top, 0), viewportHeight - 50);
2171 width: width + 'px',
2172 height: height + 'px',
2177 // 🔥 WICHTIG: !important setzen
2178 if (cfg.minHeight == undefined || cfg.minHeight == null) {
2179 el.style.setProperty('min-height', '200px', 'important');
2182 if (this.isMobileViewport()) {
2183 this.applyMobileWindowMode(windowData);
2187 initWindowEvents(windowId, cfg) {
2188 const windowData = this.windows.get(windowId);
2189 if (!windowData) return;
2191 const $win = windowData.element;
2193 $win.off('mousedown.front').on('mousedown.front', () => {
2194 this.bringToFront(windowId);
2197 $win.find(".dbx-window-close")
2198 .off('click.close touchend.close')
2199 .on('click.close touchend.close', (e) => {
2201 e.stopPropagation();
2202 this.closeWindow(windowId);
2205 $win.attr('tabindex', '-1');
2208 // --------------------------------------------------
2209 // FENSTER SCHLIESSEN
2210 // --------------------------------------------------
2212 closeWindow(windowId) {
2213 const windowData = this.windows.get(windowId);
2214 if (!windowData) return;
2216 const $win = windowData.element;
2217 const cfg = windowData.cfg;
2219 this.safeLog("close", windowId);
2221 let shouldClose = true;
2222 if (cfg.onBeforeClose) {
2223 const result = this.triggerCallbackWithReturn(cfg.onBeforeClose, $win[0]);
2224 if (result == false) shouldClose = false;
2227 if (!shouldClose) return;
2229 windowData.closing = true;
2230 this.removeDockItem(windowId);
2232 if (windowData.overlay) {
2233 windowData.overlay.fadeOut(CONFIG.ANIMATION_DURATION, () => {
2234 windowData.overlay.remove();
2238 this.animateElement($win, this.getAnimationName(cfg, "close"), cfg, () => {
2240 $(document).off(`.${windowId}`);
2244 const idx = this.stack.findIndex(w => w.id == windowId);
2245 if (idx != -1) this.stack.splice(idx, 1);
2247 this.windows.delete(windowId);
2248 this.updateAllZIndices();
2249 this.updateDockUi();
2250 this.scheduleSaveState();
2252 // --------------------------------------------------
2253 // 🔥 NEU: AFTER CLOSE ACTIONS
2254 // --------------------------------------------------
2256 if (cfg.afterClose) {
2258 this.safeLog("afterClose", cfg.afterClose);
2260 // reload dieses fensters (falls es noch existiert → skip)
2261 if (cfg.afterClose === "reload") {
2262 if (windowData.id && this.windows.has(windowData.id)) {
2263 this.reloadWindow(windowData.id);
2267 // reload alle fenster
2268 if (cfg.afterClose === "reloadAll") {
2269 this.getAllWindows().forEach(w => {
2270 if (w && w.id) this.reloadWindow(w.id);
2275 if (typeof cfg.afterClose === "string" && window[cfg.afterClose]) {
2276 window[cfg.afterClose]();
2279 if (typeof cfg.afterClose === "function") {
2284 this.triggerCallback(cfg.onClose);
2285 this.safeLog("closed", windowId);
2289 resolve(windowId, value) {
2290 const windowData = this.windows.get(windowId);
2291 if (!windowData) return;
2293 const cfg = windowData.cfg;
2294 const callerEl = windowData.callerEl;
2296 this.safeLog("resolve", windowId, value);
2299 let outValue = value;
2301 if (Array.isArray(value)) {
2302 outValue = value.map(v => v.value).join(",");
2303 } else if (value && typeof value === "object") {
2304 outValue = value.value;
2308 if (cfg.mode === "prompt") {
2310 if ("value" in callerEl) {
2311 callerEl.value = outValue;
2313 $(callerEl).trigger("change");
2316 if (cfg.keepOpen != 1) {
2317 this.closeWindow(windowId);
2326 let name = cfg.event;
2328 if (cfg.target && cfg.target !== "broadcast") {
2329 name = cfg.target + ":" + cfg.event;
2332 dbx.event.emit(name, value);
2336 // --------------------------------------------------
2338 // --------------------------------------------------
2340 openWindow(el, cfg) {
2341 el = this.domElement(el);
2342 cfg = this.normalizeConfig(cfg || {});
2343 if (cfg.title !== undefined && cfg.title !== null) {
2344 cfg.title = this.decodeConfigText(cfg.title);
2347 let url = cfg.url || (el ? el.getAttribute("href") : null);
2348 if (!url && (cfg.content !== undefined || cfg.html !== undefined)) {
2349 url = "about:blank";
2356 this.safeWarn("no url");
2360 const mode = (cfg.mode || "panel").toLowerCase();
2362 if (String(cfg.target || "").toLowerCase() == "self") {
2363 const replaced = this.replaceSelfWindowContent(el, cfg, url);
2364 if (replaced) return replaced;
2367 if (mode == "popup") {
2368 return this.openPopup(url, cfg);
2371 if (cfg.ajax != 0) {
2372 url = this.appendAjaxFlag(url);
2375 if (cfg.reuse != 0 && cfg.allowDuplicate != 1) {
2376 const fingerprint = this.getWindowFingerprint(cfg, url);
2377 const existing = this.findReusableWindow(fingerprint);
2380 this.safeLog("reuse window", existing.id, url);
2381 this.restoreWindow(existing.id);
2382 this.triggerCallback(cfg.onReuse, existing.element[0]);
2387 return this.createWindow(cfg, url, el); // 🔥 NEU
2390 openPopup(url, cfg) {
2391 const width = this.parseSize(cfg.width, screen.width, CONFIG.DEFAULT_POPUP_WIDTH);
2392 const height = this.parseSize(cfg.height, screen.height, CONFIG.DEFAULT_POPUP_HEIGHT);
2393 const left = (screen.width - width) / 2;
2394 const top = (screen.height - height) / 2;
2397 `width=${width}`, `height=${height}`,
2398 `left=${left}`, `top=${top}`,
2399 cfg.resizable != 0 ? 'resizable=yes' : 'resizable=no',
2400 cfg.scrollbars != 0 ? 'scrollbars=yes' : 'scrollbars=no',
2401 'toolbar=no', 'menubar=no', 'location=no', 'status=no'
2404 const winName = cfg.windowName || `dbx_popup_${Date.now()}`;
2405 const win = window.open(url, winName, features);
2407 if (!win && cfg.fallback != 0) {
2408 if (cfg.fallback == 'redirect') {
2409 window.location.href = url;
2411 window.open(url, '_blank');
2419 // Öffentliche Methoden
2420 getWindow(windowId) { return this.windows.get(windowId); }
2421 getAllWindows() { return Array.from(this.windows.values()); }
2422 closeAllWindows() { Array.from(this.windows.keys()).forEach(id => this.closeWindow(id)); }
2423 getWindowCount() { return this.windows.size; }
2427 // --------------------------------------------------
2428 // GLOBALE INITIALISIERUNG
2429 // --------------------------------------------------
2431 function registerOpenWinFeature(windowManager) {
2432 if (!windowManager || !dbx.feature || !dbx.feature.register) return;
2433 if (dbx.feature.has && dbx.feature.has("openWin")) return;
2435 dbx.feature.register("openWin", {
2441 ['css', 'design', 'c-openWin.css']
2445 ['js', 'lib', 'ajax.js']
2448 init: (el, cfg) => {
2450 el.__dbxInitialized = el.__dbxInitialized || {};
2451 if (el.__dbxInitialized["openWin"]) return;
2453 windowManager.restoreState();
2458 if (!dbx.__openWinManager) {
2460 const windowManager = new WindowManager();
2461 dbx.__openWinManagerInstance = windowManager;
2465 open: (cfg, el) => windowManager.openWindow(el || null, cfg),
2466 close: (windowId) => windowManager.closeWindow(windowId),
2467 closeAll: () => windowManager.closeAllWindows(),
2468 get: (windowId) => windowManager.getWindow(windowId),
2469 getAll: () => windowManager.getAllWindows(),
2470 getCount: () => windowManager.getWindowCount(),
2471 bringToFront: (windowId) => windowManager.bringToFront(windowId),
2472 popup: (url, cfg) => windowManager.openPopup(url, cfg || {}),
2473 saveState: () => windowManager.saveState(true),
2474 restoreState: (options) => windowManager.restoreState(options || {}),
2475 clearState: () => windowManager.clearState(),
2476 getState: () => windowManager.readState(),
2477 resolve: (windowId, value) => windowManager.resolve(windowId, value) // 🔥 NEU
2480 // Feature registrieren
2481 registerOpenWinFeature(windowManager);
2483 setTimeout(() => windowManager.restoreState(), 120);
2485 dbx.__openWinManager = true;
2487 windowManager.safeLog("ready - modal z-index:", CONFIG.BASE_Z_MODAL,
2488 "dragable z-index:", CONFIG.BASE_Z_DRAGGABLE);
2491 registerOpenWinFeature(dbx.__openWinManagerInstance);
2492 setTimeout(() => registerOpenWinFeature(dbx.__openWinManagerInstance), 0);
2493 setTimeout(() => registerOpenWinFeature(dbx.__openWinManagerInstance), 80);
2495 if (dbx.declare && typeof dbx.declare.registerSchema === "function") {
2497 dbx.declare.registerSchema("openWin", {
2501 aliases: ["data-url"],
2502 infer: function (el) {
2503 return el && el.getAttribute ? (el.getAttribute("href") || "") : "";
2508 aliases: ["data-title"],
2509 infer: function (el) {
2510 return el && el.getAttribute ? (el.getAttribute("title") || "") : "";
2515 aliases: ["data-height", "data-size-y"]
2519 aliases: ["data-width", "data-size-x"]
2523 aliases: ["data-size-x"]
2527 aliases: ["data-size-y"]
2531 aliases: ["data-modal"]
2535 aliases: ["data-scroll"]
2539 aliases: ["data-position"]
2543 aliases: ["data-minimizable"]
2547 aliases: ["data-maximizable"]
2551 aliases: ["data-resizable"]
2556 dbx.declare.transforms.openWin = function (raw) {
2557 const width = raw.width || raw.sizeX || "1280";
2558 const height = raw.height || raw.sizeY || "760";
2561 url: String(raw.url || "").trim(),
2562 title: String(raw.title || "Fenster").trim(),
2565 sizeX: raw.sizeX || width,
2566 sizeY: raw.sizeY || height,
2569 position: String(raw.position || "center").trim(),
2572 resizable: raw.resizable,
2573 minimizable: raw.minimizable,
2574 maximizable: raw.maximizable
2579})(window, document, jQuery);