dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
openWin.js
Go to the documentation of this file.
1/**
2 * openWin.js - Fenster-Manager Bibliothek
3 * Version: 2.1.0
4 * Core-unabhängiges CSS-Loading
5 */
6
7(function (window, document, $) {
8 "use strict";
9
10 if (!window.dbx || !$) {
11 console.warn("openWin: dbx oder jQuery nicht verfügbar");
12 return;
13 }
14
15 const dbx = window.dbx;
16
17
18 // --------------------------------------------------
19 // KONSTANTEN
20 // --------------------------------------------------
21
22 const CONFIG = {
23 // Z-Index Basiswerte: normale openWin-Fenster bleiben hoch,
24 // echte Werkzeug-/Optionsdialoge bekommen bei Bedarf die Top-Ebene.
25 BASE_Z_DRAGGABLE: 70000,
26 BASE_Z_MODAL: 80000,
27 BASE_Z_TOP: 230000,
28 Z_INCREMENT: 20,
29 ANIMATION_DURATION: 200,
30 MAX_WINDOWS: 15,
31 DEFAULT_WIDTH: 0.7,
32 DEFAULT_HEIGHT: 0.6,
33 MIN_WIDTH: 200,
34 MIN_HEIGHT: 150,
35 RESIZE_HANDLE_SIZE: 12,
36 DRAG_OFFSET: 30,
37 DEFAULT_POPUP_WIDTH: 800,
38 DEFAULT_POPUP_HEIGHT: 600,
39 STATE_VERSION: 1,
40 STATE_KEY: "dbx.openWin.state",
41 STATE_SAVE_DELAY: 120
42 };
43
44 // --------------------------------------------------
45 // WINDOW MANAGER KLASSE
46 // --------------------------------------------------
47
48 class WindowManager {
49
50 constructor() {
51 this.windows = new Map();
52 this.stack = [];
53 this.activeDrag = null;
54 this.activeResize = null;
55 this.counter = 0;
56 this.cssLoaded = false;
57 this.stateSaveTimer = null;
58 this.restoringState = false;
59 this.stateRestored = false;
60
61 this.initEventSystem();
62 this.initGlobalDelegation();
63
64 }
65
66 // --------------------------------------------------
67 // EVENT SYSTEM (ZENTRAL)
68 // --------------------------------------------------
69
70 initEventSystem() {
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', () => {
77 this.saveState(true);
78 });
79 }
80
81 initGlobalDelegation() {
82 dbx.on("click", ".dbx-win, [data-dbx*='lib=openWin']", (e, el) => {
83 this.handleWinClick(e, el);
84 });
85
86 // 🔥 NEU: Window Actions
87 dbx.on("click", ".dbx-window [data-action]", (e, el) => {
88 this.handleWindowAction(e, el);
89 });
90
91 $(document).on("click.openWinDock", "#windrop .dbx-window-drop", (e) => {
92 e.preventDefault();
93 const windowId = $(e.currentTarget).attr("data-window-id");
94 this.restoreWindow(windowId);
95 });
96
97 $(document).on("click.openWinCloseAll", "#dbxWindowCloseAll", (e) => {
98 e.preventDefault();
99 this.closeAllWindows();
100 });
101 }
102
103 handleWindowAction(e, el) {
104 const action = el.dataset.action;
105 if (!action) return;
106
107 const $win = $(el).closest(".dbx-window");
108 if (!$win.length) return;
109
110 const windowId = $win.attr("id");
111 const windowData = this.windows.get(windowId);
112 if (!windowData) return;
113
114 this.safeLog("action", action, windowId);
115
116 switch(action) {
117
118 case "close":
119 this.closeWindow(windowId);
120 break;
121
122 case "reload":
123 this.reloadWindow(windowId);
124 break;
125
126 case "fullscreen":
127 $win.find(".dbx-window-maximize").trigger("click");
128 break;
129
130 case "minimize":
131 if (!this.isMobileViewport()) {
132 $win.find(".dbx-window-minimize").trigger("click");
133 }
134 break;
135
136 case "front":
137 this.bringToFront(windowId);
138 break;
139
140 case "select":
141
142 let value = el.dataset.value;
143 let label = el.dataset.label;
144
145 if (value === undefined) {
146 value = $(el).text().trim();
147 }
148
149 if (label === undefined) {
150 label = value;
151 }
152
153 const item = { value, label };
154
155 // 🔥 MULTI MODE
156 if (windowData.cfg.multi == 1) {
157
158 const idx = windowData.selectedValues.findIndex(v => v.value == value);
159
160 if (idx !== -1) {
161 windowData.selectedValues.splice(idx, 1);
162 $(el).removeClass("dbx-selected");
163 } else {
164 windowData.selectedValues.push(item);
165 $(el).addClass("dbx-selected");
166 }
167
168 // 🔥 keepOpen entscheidet ob direkt resolved wird
169 if (windowData.cfg.keepOpen != 1) {
170 this.resolve(windowId, windowData.selectedValues);
171 }
172
173 } else {
174 this.resolve(windowId, item);
175 }
176
177 break;
178 }
179
180 e.preventDefault();
181 }
182
183 readWinConfig(el) {
184
185 if (!el) return null;
186
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];
190 }
191
192 const raw = el.getAttribute("data-dbx");
193 if (!raw) return null;
194
195 const cfgList = dbx.parseData(raw);
196 return cfgList.find(c => c.lib == "openWin") || null;
197 }
198
199 handleWinClick(e, el) {
200
201 const cfg = this.readWinConfig(el);
202 if (!cfg) return;
203
204 e.preventDefault();
205
206 if (el.__dbxOpening) return;
207 el.__dbxOpening = true;
208 setTimeout(() => el.__dbxOpening = false, 300);
209
210 this.openWindow(el, cfg);
211 }
212
213 // --------------------------------------------------
214 // MOUSE EVENT HANDLER
215 // --------------------------------------------------
216
217 handleMouseMove(e) {
218 if (this.activeDrag) {
219 this.updateDragPosition(e);
220 }
221 if (this.activeResize) {
222 this.updateResizeSize(e);
223 }
224 }
225
226 handleMouseUp(e) {
227
228 // 🔥 NEU: dragging / resizing cleanup
229 const changedWindowId = this.activeDrag?.windowId || this.activeResize?.windowId || null;
230
231 if (this.activeDrag) {
232 const w = this.windows.get(this.activeDrag.windowId);
233 if (w) w.element.removeClass("dbx-window-dragging");
234 }
235
236 if (this.activeResize) {
237 const w = this.windows.get(this.activeResize.windowId);
238 if (w) w.element.removeClass("dbx-window-resizing");
239 }
240
241 this.activeDrag = null;
242 this.activeResize = null;
243
244 if (changedWindowId) {
245 this.scheduleSaveState();
246 }
247 }
248
249 handleKeyDown(e) {
250
251 // 🔥 PROMPT KEYBOARD + TYPEAHEAD
252 if (this.stack.length > 0) {
253
254 const activeWin = this.stack[this.stack.length - 1];
255 if (activeWin && activeWin.cfg.mode === "prompt") {
256
257 const $win = activeWin.element;
258 const $allItems = $win.find("[data-action='select']"); // 🔥 FIX
259 const $items = $allItems.filter(":visible"); // 🔥 FIX
260
261 if ($items.length) {
262
263 // 🔥 CONFIG
264 const filterMode = activeWin.cfg.filter || "highlight";
265
266 // 🔥 INIT TYPE BUFFER
267 if (!activeWin._typeBuffer) activeWin._typeBuffer = "";
268 if (!activeWin._typeTimer) activeWin._typeTimer = null;
269
270 let $current = $items.filter(".dbx-active").first();
271
272 if (!$current.length) {
273 $items.removeClass("dbx-active"); // 🔥 FIX
274 $current = $items.first().addClass("dbx-active");
275 }
276
277 let idx = $items.index($current);
278
279 if (idx === -1) idx = 0; // 🔥 FIX
280
281 // --------------------------------------------------
282 // 🔥 TYPEAHEAD
283 // --------------------------------------------------
284 if (e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
285
286 activeWin._typeBuffer += e.key.toLowerCase();
287
288 clearTimeout(activeWin._typeTimer);
289 activeWin._typeTimer = setTimeout(() => {
290
291 activeWin._typeBuffer = "";
292
293 // 🔥 RESET FILTER (ALLE ITEMS!)
294 $allItems.each(function() { // 🔥 FIX
295 const $el = $(this);
296 $el.show();
297 $el.removeClass("dbx-match");
298 });
299
300 }, 500);
301
302 const search = activeWin._typeBuffer;
303
304 let foundIndex = -1;
305
306 $allItems.each(function(i) { // 🔥 FIX
307 const $el = $(this);
308
309 const label = ($el.data("label") || $el.text()).toString().toLowerCase();
310
311 const match = label.includes(search);
312
313 // 🔥 FILTER MODES
314 if (filterMode === "hide") {
315 $el.toggle(match);
316 } else {
317 $el.show();
318 }
319
320 if (filterMode === "highlight") {
321 $el.toggleClass("dbx-match", match);
322 } else {
323 $el.removeClass("dbx-match");
324 }
325
326 if (match && foundIndex === -1) {
327 foundIndex = i;
328 }
329 });
330
331 const $visible = $allItems.filter(":visible"); // 🔥 FIX
332
333 if (foundIndex !== -1 && $visible.length) {
334
335 $allItems.removeClass("dbx-active"); // 🔥 FIX
336
337 const $target = $visible.first(); // 🔥 FIX (stabil)
338
339 $target.addClass("dbx-active");
340
341 const el = $target[0];
342 if (el && el.scrollIntoView) {
343 el.scrollIntoView({ block: "nearest" });
344 }
345 }
346
347 e.preventDefault();
348 return;
349 }
350
351 // --------------------------------------------------
352 // 🔥 ARROWS
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");
358 e.preventDefault();
359 return;
360 }
361
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");
366 e.preventDefault();
367 return;
368 }
369
370 // --------------------------------------------------
371 // 🔥 ENTER
372 // --------------------------------------------------
373 if (e.key === "Enter") {
374 $items.eq(idx).trigger("click");
375 e.preventDefault();
376 return;
377 }
378 }
379 }
380 }
381
382 // --------------------------------------------------
383 // BESTEHENDES VERHALTEN (UNVERÄNDERT)
384 // --------------------------------------------------
385
386 if (e.key == 'Escape' && this.stack.length > 0) {
387
388 const activeWin = this.stack[this.stack.length - 1];
389 if (!activeWin) return;
390
391 if (activeWin.fullscreen && !this.isMobileViewport()) {
392
393 const $win = activeWin.element;
394
395 if (activeWin.originalSize) {
396 $win.css({
397 width: activeWin.originalSize.width,
398 height: activeWin.originalSize.height,
399 left: activeWin.originalPosition.left,
400 top: activeWin.originalPosition.top
401 });
402 }
403
404 activeWin.fullscreen = false;
405 $win.removeClass("dbx-window-fullscreen");
406
407 $win.find(".dbx-window-maximize")
408 .html('<i class="bi bi-square"></i>');
409
410 e.preventDefault();
411 return;
412 }
413
414 if (activeWin.cfg.closable != 0) {
415 this.closeWindow(activeWin.id);
416 e.preventDefault();
417 }
418 }
419
420 const activeElement = document.activeElement;
421 if (activeElement && activeElement.closest) {
422 if (this.isKeyboardEditingElement(activeElement)) return;
423 const windowEl = activeElement.closest('.dbx-window');
424 if (windowEl) {
425 this.moveWindowWithKeys(windowEl.id, e);
426 }
427 }
428 }
429
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);
437 }
438 return true;
439 }
440
441 moveWindowWithKeys(windowId, e) {
442 const windowData = this.windows.get(windowId);
443 if (!windowData || windowData.cfg.draggable == 0) return;
444 if (this.isMobileViewport()) return;
445
446 const step = e.shiftKey ? 10 : 1;
447 const currentPos = windowData.element.position();
448 let newLeft = currentPos.left;
449 let newTop = currentPos.top;
450
451 switch(e.key) {
452 case 'ArrowLeft': newLeft -= step; break;
453 case 'ArrowRight': newLeft += step; break;
454 case 'ArrowUp': newTop -= step; break;
455 case 'ArrowDown': newTop += step; break;
456 default: return;
457 }
458
459 const maxLeft = window.innerWidth - windowData.element.outerWidth();
460 const maxTop = window.innerHeight - windowData.element.outerHeight();
461
462 newLeft = Math.min(Math.max(newLeft, 0), maxLeft);
463 newTop = Math.min(Math.max(newTop, 0), maxTop);
464
465 windowData.element.css({ left: newLeft + 'px', top: newTop + 'px' });
466 e.preventDefault();
467 }
468
469 // --------------------------------------------------
470 // Z-INDEX MANAGEMENT
471 // --------------------------------------------------
472
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";
477 }
478
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;
483 }
484
485 domElement(el) {
486 if (!el) return null;
487 if (el.jquery && el[0]) return el[0];
488 if (el.nodeType === 1) return el;
489 return null;
490 }
491
492 numericZIndex(el) {
493 el = this.domElement(el);
494 if (!el) return 0;
495 const z = parseInt(window.getComputedStyle(el).zIndex, 10);
496 return Number.isFinite(z) ? z : 0;
497 }
498
499 maxElementZIndex(el) {
500 let max = 0;
501 let cur = this.domElement(el);
502 while (cur && cur !== document.documentElement) {
503 max = Math.max(max, this.numericZIndex(cur));
504 cur = cur.parentElement;
505 }
506 return max;
507 }
508
509 callerZIndexFloor(callerEl) {
510 return this.maxElementZIndex(callerEl);
511 }
512
513 nextZAboveCaller(zIndex, callerEl) {
514 const callerZ = this.callerZIndexFloor(callerEl);
515 if (callerZ > 0) {
516 zIndex = Math.max(zIndex, callerZ + CONFIG.Z_INCREMENT);
517 }
518 return Math.min(2147483647, zIndex);
519 }
520
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);
524 }
525
526 bringToFront(windowId) {
527 const windowData = this.windows.get(windowId);
528 if (!windowData) return;
529
530 const idx = this.stack.findIndex(w => w.id == windowId);
531 if (idx != -1) {
532 const [win] = this.stack.splice(idx, 1);
533 this.stack.push(win);
534 }
535
536 this.windows.forEach(w => {
537 w.element.removeClass("dbx-window-active");
538 });
539
540 windowData.element.addClass("dbx-window-active");
541 this.updateAllZIndices();
542
543 const newZIndex = parseInt(windowData.element.css('z-index'), 10) || 0;
544 this.safeLog("bringToFront", windowId, "z-index:", newZIndex);
545 this.scheduleSaveState();
546 }
547
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;
554
555 this.bringToFront(windowData.id);
556 return;
557 }
558
559 this.windows.forEach(w => {
560 w.element.removeClass("dbx-window-active");
561 });
562 this.updateAllZIndices();
563 }
564
565 focusWindow(windowId) {
566 const windowData = this.windows.get(windowId);
567 if (!windowData) return;
568
569 const $win = windowData.element;
570 const $active = $win.find("input,textarea,select,button,a,[tabindex]").filter(":visible").first();
571
572 if ($active.length) {
573 $active.focus();
574 } else {
575 $win.focus();
576 }
577 }
578
579 restoreWindow(windowId) {
580 const windowData = this.windows.get(windowId);
581 if (!windowData) return;
582
583 if (this.isMobileViewport()) {
584 this.applyMobileWindowMode(windowData);
585 this.bringToFront(windowId);
586 this.focusWindow(windowId);
587 return;
588 }
589
590 const $win = windowData.element;
591
592 if (windowData.minimized || !$win.is(":visible")) {
593 const $dockItem = this.getDockItem(windowId);
594 const fromRect = this.getElementRect($dockItem);
595
596 if (windowData.maximized && windowData.originalSize && windowData.originalPosition) {
597 $win.css({
598 width: windowData.originalSize.width,
599 height: windowData.originalSize.height,
600 left: windowData.originalPosition.left,
601 top: windowData.originalPosition.top
602 });
603 windowData.maximized = false;
604 $win.find(".dbx-window-maximize").html('<i class="bi bi-square"></i>');
605 }
606
607 if (windowData.overlay) {
608 windowData.overlay.show();
609 }
610
611 $win.stop(true, true).css("display", "flex");
612 $win.css("visibility", "hidden");
613 const toRect = this.getElementRect($win);
614
615 windowData.minimized = false;
616 $win.removeClass("dbx-window-is-minimized");
617 $win.find(".dbx-window-minimize").html('<i class="bi bi-dash"></i>');
618
619 this.animateDockTransfer($win, fromRect, toRect, windowData.cfg, "restore", () => {
620 this.removeDockItem(windowId);
621 this.focusWindow(windowId);
622 this.scheduleSaveState();
623 });
624 }
625
626 this.bringToFront(windowId);
627 this.focusWindow(windowId);
628 }
629
630 getDock() {
631 let $dock = $("#windrop");
632
633 if (!$dock.length) {
634 $dock = $('<div id="windrop"></div>');
635
636 const $footer = $("#dbxFooter");
637 if ($footer.length) {
638 $footer.prepend($dock);
639 } else {
640 $("body").append($dock.addClass("dbx-window-dock-floating"));
641 }
642 }
643
644 if (!$dock.attr("aria-label")) {
645 $dock.attr("aria-label", "Minimierte Fenster");
646 }
647
648 return $dock.addClass("dbx-window-dock");
649 }
650
651 updateDockUi() {
652 const $footer = $("#dbxFooter");
653 if (!$footer.length) return;
654
655 const windowCount = this.windows.size;
656 const dockCount = $("#windrop .dbx-window-drop").length;
657 const $closeAll = $("#dbxWindowCloseAll");
658
659 $footer
660 .toggleClass("has-open-windows", windowCount > 0)
661 .toggleClass("has-docked-windows", dockCount > 0);
662
663 if ($closeAll.length) {
664 $closeAll
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");
668 }
669 }
670
671 getDockItem(windowId) {
672 if (!windowId) return $();
673
674 return this.getDock()
675 .children(".dbx-window-drop")
676 .filter((idx, el) => el.getAttribute("data-window-id") == windowId)
677 .first();
678 }
679
680 createDockItem(windowData) {
681 const title = this.getDockTitle(windowData);
682 let $item = this.getDockItem(windowData.id);
683
684 if (!$item.length) {
685 $item = $(`
686 <button type="button" class="dbx-window-drop">
687 <i class="bi bi-window"></i>
688 <span class="dbx-window-drop-title"></span>
689 </button>
690 `);
691
692 this.getDock().append($item);
693 }
694
695 $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");
701
702 $item.find(".dbx-window-drop-title").text(title);
703 this.updateDockUi();
704
705 return $item;
706 }
707
708 getDockTitle(windowData) {
709 if (!windowData) return "Fenster";
710
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();
714 }
715
716 removeDockItem(windowId) {
717 this.getDockItem(windowId).remove();
718 this.updateDockUi();
719 }
720
721 getElementRect($el) {
722 if (!$el || !$el.length || !$el[0].getBoundingClientRect) return null;
723
724 const rect = $el[0].getBoundingClientRect();
725 if (!rect.width || !rect.height) return null;
726
727 return {
728 left: rect.left,
729 top: rect.top,
730 width: rect.width,
731 height: rect.height
732 };
733 }
734
735 getDockAnimationDuration(cfg) {
736 cfg = cfg || {};
737
738 const duration = parseInt(
739 cfg.dockAnimDuration ||
740 cfg.dockAnimationDuration ||
741 cfg.windropAnimDuration ||
742 cfg.windropAnimationDuration,
743 10
744 );
745
746 if (!isNaN(duration)) return Math.max(320, duration);
747
748 return Math.max(680, this.getAnimationDuration(cfg));
749 }
750
751 animateDockTransfer($win, fromRect, toRect, cfg, mode, done) {
752 const duration = this.getDockAnimationDuration(cfg);
753
754 if (!$win || !$win.length || !fromRect || !toRect || duration <= 0 || !window.requestAnimationFrame) {
755 if ($win && $win.length) {
756 $win.css("visibility", "");
757 }
758 if (done) done();
759 return;
760 }
761
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})`;
774 const el = $win[0];
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;
780
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;
787 };
788
789 const finish = () => {
790 if (finished) return;
791 finished = true;
792 clearTimeout(timer);
793 $win.off("transitionend.openWinDock");
794
795 if (mode == "minimize") {
796 if (done) done();
797 cleanup();
798 return;
799 }
800
801 cleanup();
802 if (done) done();
803 };
804
805 $win
806 .addClass("dbx-window-docking")
807 .css({
808 visibility: "",
809 transition: "none",
810 transformOrigin: "left top",
811 transform: startTransform,
812 pointerEvents: "none"
813 });
814
815 el.offsetHeight;
816
817 window.requestAnimationFrame(() => {
818 window.requestAnimationFrame(() => {
819 $win.css({
820 transition: `transform ${duration}ms cubic-bezier(0.18, 0.86, 0.24, 1)`,
821 transform: endTransform
822 });
823 });
824 });
825
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;
830 finish();
831 });
832 }
833
834 updateAllZIndices() {
835 this.stack.forEach((windowData, idx) => {
836 const baseZ = this.windowBaseZ(windowData, windowData.isModal);
837
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);
842 }
843 });
844
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);
853 }
854 });
855 }
856 }
857
858 reloadWindow(windowId) {
859 const windowData = this.windows.get(windowId);
860 if (!windowData) return;
861
862 let url = windowData.url;
863 if (!url) return;
864
865 // 🔥 cache bust (optional, aber sinnvoll)
866 const sep = url.includes("?") ? "&" : "?";
867 url = url + sep + "_=" + Date.now();
868
869 this.safeLog("reload", windowId, url);
870
871 this.loadContent(windowId, url, windowData.cfg);
872 }
873
874 // --------------------------------------------------
875 // HILFSFUNKTIONEN
876 // --------------------------------------------------
877
878 safeLog(...args) {
879 if (dbx.log && typeof dbx.log == 'function') {
880 dbx.log("openWin →", ...args);
881 } else if (console && console.log) {
882 console.log("[openWin]", ...args);
883 }
884 }
885
886 safeWarn(...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);
891 }
892 }
893
894 parseSize(value, base, defaultValue = null) {
895 if (value == undefined || value == null) return defaultValue;
896
897 const strValue = value.toString().trim().toLowerCase();
898
899 if (strValue == "auto") return "auto";
900
901 if (strValue.endsWith("%")) {
902 const p = parseFloat(strValue);
903 if (!isNaN(p)) return Math.round(base * p / 100);
904 }
905
906 let numericValue = parseInt(strValue);
907 if (strValue.endsWith("px")) {
908 numericValue = parseInt(strValue);
909 }
910
911 if (!isNaN(numericValue)) return numericValue;
912
913 return defaultValue;
914 }
915
916 optionEnabled(value, defaultValue = true) {
917 if (value == undefined || value == null || value === "") return defaultValue;
918 if (value === false) return false;
919
920 const strValue = value.toString().trim().toLowerCase();
921 return !["0", "false", "off", "no", "nein"].includes(strValue);
922 }
923
924 optionUnset(value) {
925 return value == undefined || value == null || value === "";
926 }
927
928 isMobileViewport() {
929 const maxWidth = 768;
930
931 if (window.matchMedia) {
932 return window.matchMedia("(max-width: " + maxWidth + "px)").matches;
933 }
934
935 return window.innerWidth <= maxWidth;
936 }
937
938 applyMobileWindowMode(windowData) {
939 if (!windowData || !this.isMobileViewport()) return;
940
941 const $win = windowData.element;
942 if (!$win || !$win.length) return;
943
944 const el = $win[0];
945 const heightValue = (
946 window.CSS &&
947 CSS.supports &&
948 CSS.supports("height", "100dvh")
949 ) ? "100dvh" : window.innerHeight + "px";
950
951 windowData.minimized = false;
952 windowData.fullscreen = true;
953 windowData.maximized = true;
954
955 this.removeDockItem(windowData.id);
956
957 $win
958 .removeClass("dbx-window-is-minimized")
959 .addClass("dbx-window-fullscreen dbx-window-mobile-fullscreen")
960 .css("display", "flex");
961
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");
971
972 $win.find(".dbx-window-minimize").hide();
973 $win.find(".dbx-window-maximize").hide();
974 $win.find(".dbx-window-resize").hide();
975
976 if (windowData.overlay) {
977 windowData.overlay.show();
978 }
979 }
980
981 normalizeConfig(cfg) {
982 cfg = cfg || {};
983
984 if (this.optionUnset(cfg.scroll)) {
985 cfg.scroll = 1;
986 }
987
988 if (this.optionUnset(cfg.reloadable) && this.optionUnset(cfg.reload)) {
989 cfg.reload = 1;
990 }
991
992 return cfg;
993 }
994
995 getStateStorageKey() {
996 const path = (window.location && window.location.pathname) ? window.location.pathname : "default";
997 return CONFIG.STATE_KEY + ":" + path;
998 }
999
1000 readState() {
1001 try {
1002 if (!dbx.uiGet) return null;
1003
1004 const data = dbx.uiGet("openWin", this.getStateStorageKey(), "state", null);
1005 if (!data || data.version !== CONFIG.STATE_VERSION || !Array.isArray(data.windows)) {
1006 return null;
1007 }
1008
1009 return data;
1010 } catch (e) {
1011 this.safeWarn("state read failed", e);
1012 return null;
1013 }
1014 }
1015
1016 writeState(data) {
1017 try {
1018 if (!dbx.uiSet) return false;
1019 dbx.uiSet("openWin", this.getStateStorageKey(), "state", data && data.windows && data.windows.length ? data : null);
1020 return true;
1021 } catch (e) {
1022 this.safeWarn("state write failed", e);
1023 return false;
1024 }
1025 }
1026
1027 clearState() {
1028 try {
1029 if (!dbx.uiSet) return false;
1030 dbx.uiSet("openWin", this.getStateStorageKey(), "state", null);
1031 return true;
1032 } catch (e) {
1033 this.safeWarn("state clear failed", e);
1034 return false;
1035 }
1036 }
1037
1038 getRestoreMode(options = {}) {
1039 const configMode = dbx.config && (
1040 dbx.config.openWinRestoreMode ||
1041 (dbx.config.openWin && dbx.config.openWin.restoreMode)
1042 );
1043 const mode = (options.mode || options.restoreMode || configMode || "minimized").toString().toLowerCase();
1044
1045 return mode == "minimized" || mode == "minimize" || mode == "windrop" ? "minimized" : "state";
1046 }
1047
1048 isPersistableWindow(windowData) {
1049 if (!windowData || !windowData.url) return false;
1050 if (windowData.closing) return false;
1051
1052 const cfg = windowData.cfg || {};
1053 const mode = (cfg.mode || "panel").toString().toLowerCase();
1054 const method = (cfg.method || "GET").toString().toUpperCase();
1055
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;
1060
1061 return true;
1062 }
1063
1064 sanitizeStateConfig(cfg) {
1065 const skipKeys = [
1066 "restore", "__restore", "__restoreState", "__restoreMinimized",
1067 "callerEl", "onBeforeClose", "onOpen", "onClose", "onLoad",
1068 "onError", "onReuse", "onMaxReached"
1069 ];
1070
1071 try {
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;
1077 return value;
1078 }));
1079 } catch (e) {
1080 const clean = {};
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;
1086 clean[key] = value;
1087 });
1088 return clean;
1089 }
1090 }
1091
1092 getWindowState(windowData, order) {
1093 if (!this.isPersistableWindow(windowData)) return null;
1094
1095 const $win = windowData.element;
1096 const $body = $win.find(".dbx-window-body");
1097
1098 return {
1099 url: windowData.url,
1100 cfg: this.sanitizeStateConfig(windowData.cfg),
1101 fingerprint: windowData.fingerprint,
1102 order: order,
1103 title: this.getDockTitle(windowData),
1104 state: {
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
1115 }
1116 };
1117 }
1118
1119 saveState(force = false) {
1120 if (this.restoringState && !force) return false;
1121
1122 clearTimeout(this.stateSaveTimer);
1123 this.stateSaveTimer = null;
1124
1125 const windows = this.stack
1126 .map((windowData, order) => this.getWindowState(windowData, order))
1127 .filter(Boolean);
1128
1129 return this.writeState({
1130 version: CONFIG.STATE_VERSION,
1131 savedAt: Date.now(),
1132 path: window.location ? window.location.pathname : "",
1133 windows: windows
1134 });
1135 }
1136
1137 scheduleSaveState() {
1138 if (this.restoringState) return;
1139
1140 clearTimeout(this.stateSaveTimer);
1141 this.stateSaveTimer = setTimeout(() => this.saveState(), CONFIG.STATE_SAVE_DELAY);
1142 }
1143
1144 applySavedWindowState(windowData, state) {
1145 if (!windowData || !state) return;
1146
1147 const $win = windowData.element;
1148 const css = {};
1149
1150 ["left", "top", "width", "height"].forEach(key => {
1151 if (state[key] !== undefined && state[key] !== null && state[key] !== "") {
1152 css[key] = state[key];
1153 }
1154 });
1155
1156 if (Object.keys(css).length) {
1157 $win.css(css);
1158 }
1159
1160 windowData.originalSize = state.originalSize || null;
1161 windowData.originalPosition = state.originalPosition || null;
1162
1163 if (state.fullscreen) {
1164 $win.css({
1165 width: "100%",
1166 height: "100%",
1167 left: "0",
1168 top: "0"
1169 });
1170
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>');
1174 }
1175 }
1176
1177 restoreState(options = {}) {
1178 if (this.stateRestored && !options.force) return [];
1179
1180 const data = this.readState();
1181 this.stateRestored = true;
1182
1183 if (!data || !Array.isArray(data.windows) || !data.windows.length) {
1184 return [];
1185 }
1186
1187 const mode = this.getRestoreMode(options);
1188 const restored = [];
1189
1190 this.restoringState = true;
1191
1192 data.windows
1193 .slice()
1194 .sort((a, b) => (a.order || 0) - (b.order || 0))
1195 .forEach(item => {
1196 if (!item || !item.url) return;
1197
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;
1201
1202 const fingerprint = this.getWindowFingerprint(cfg, item.url);
1203
1204 if (this.findReusableWindow(fingerprint)) return;
1205
1206 cfg.__restore = 1;
1207 cfg.__restoreState = item.state || {};
1208 cfg.__restoreMinimized = (mode == "minimized" || (item.state && item.state.minimized)) ? 1 : 0;
1209
1210 const windowId = this.createWindow(cfg, item.url, null);
1211 if (windowId) restored.push(windowId);
1212 });
1213
1214 this.restoringState = false;
1215 this.saveState(true);
1216
1217 this.safeLog("state restored", restored.length, "mode:", mode);
1218 return restored;
1219 }
1220
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";
1226 }
1227
1228 normalizeWindowUrl(url) {
1229 url = String(url || '').replace(/&amp;/g, '&');
1230
1231 try {
1232 const parsed = new URL(url, window.location.href);
1233 const entries = Array.from(parsed.searchParams.entries())
1234 .filter(([key]) => key !== '_')
1235 .sort((a, b) => {
1236 const keyCompare = a[0].localeCompare(b[0]);
1237 return keyCompare || String(a[1]).localeCompare(String(b[1]));
1238 });
1239
1240 parsed.search = '';
1241 entries.forEach(([key, value]) => parsed.searchParams.append(key, value));
1242
1243 return parsed.href;
1244 } catch (e) {
1245 return url;
1246 }
1247 }
1248
1249 getWindowFingerprint(cfg, url) {
1250 const uniqueKeys = [
1251 'mode', 'modal', 'title', 'width', 'height', 'position',
1252 'left', 'top', 'sizeX', 'sizeY', 'scroll'
1253 ];
1254 const data = {
1255 url: this.normalizeWindowUrl(url)
1256 };
1257
1258 uniqueKeys.forEach(key => {
1259 if (cfg[key] !== undefined && cfg[key] !== null) {
1260 data[key] = cfg[key];
1261 }
1262 });
1263
1264 return JSON.stringify(data);
1265 }
1266
1267 findReusableWindow(fingerprint) {
1268 if (!fingerprint) return null;
1269
1270 for (const windowData of this.windows.values()) {
1271 if (windowData.fingerprint === fingerprint) {
1272 return windowData;
1273 }
1274 }
1275
1276 return null;
1277 }
1278
1279 escapeHtml(str) {
1280 if (!str) return '';
1281 return str
1282 .replace(/&/g, '&amp;')
1283 .replace(/</g, '&lt;')
1284 .replace(/>/g, '&gt;')
1285 .replace(/"/g, '&quot;')
1286 .replace(/'/g, '&#39;');
1287 }
1288
1289 decodeConfigText(value) {
1290 if (value === undefined || value === null) return '';
1291
1292 const text = value.toString();
1293 if (text.indexOf('%') === -1) return text;
1294
1295 try {
1296 return decodeURIComponent(text);
1297 } catch (e) {
1298 return text;
1299 }
1300 }
1301
1302 getAnimationName(cfg, type) {
1303 cfg = cfg || {};
1304
1305 if (type == "open") {
1306 return cfg.animOpen || cfg.openAnimation || cfg.animationOpen || "fade-scale-in";
1307 }
1308
1309 if (type == "minimize") {
1310 return cfg.animMin || cfg.minAnimation || cfg.animationMin || cfg.minimizeAnimation || "fade-out";
1311 }
1312
1313 if (type == "close") {
1314 return cfg.animClose || cfg.closeAnimation || cfg.animationClose || "fade-out";
1315 }
1316
1317 return "none";
1318 }
1319
1320 getAnimationDuration(cfg) {
1321 const duration = parseInt((cfg || {}).animDuration || (cfg || {}).animationDuration || CONFIG.ANIMATION_DURATION, 10);
1322 return isNaN(duration) ? CONFIG.ANIMATION_DURATION : duration;
1323 }
1324
1325 normalizeAnimationName(name) {
1326 name = (name || "none").toString().trim().toLowerCase();
1327
1328 const aliases = {
1329 "fade": "fade-in",
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",
1345 "none": "none",
1346 "0": "none"
1347 };
1348
1349 return aliases[name] || name.replace(/[^a-z0-9_-]/g, "");
1350 }
1351
1352 animateElement($el, animationName, cfg, done) {
1353 const name = this.normalizeAnimationName(animationName);
1354 const duration = this.getAnimationDuration(cfg);
1355
1356 if (!$el || !$el.length || name == "none" || duration <= 0) {
1357 if (done) done();
1358 return;
1359 }
1360
1361 const className = "dbx-ow-anim-" + name;
1362 let finished = false;
1363
1364 const finish = () => {
1365 if (finished) return;
1366 finished = true;
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");
1371 if (done) done();
1372 };
1373
1374 $el.removeClass((index, classList) => {
1375 return (classList.match(/(^|\s)dbx-ow-anim-\S+/g) || []).join(" ");
1376 });
1377
1378 $el[0].style.setProperty("--dbx-openwin-animation-duration", duration + "ms");
1379 $el.addClass("dbx-ow-anim-running " + className);
1380
1381 const timer = setTimeout(finish, duration + 80);
1382 $el.off("animationend.openWinAnimation").on("animationend.openWinAnimation", finish);
1383 }
1384
1385 triggerCallback(callback, ...args) {
1386 if (callback && typeof callback == 'function') {
1387 callback(...args);
1388 } else if (callback && typeof callback == 'string' && window[callback]) {
1389 window[callback](...args);
1390 }
1391 }
1392
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);
1398 }
1399 return undefined;
1400 }
1401
1402 // --------------------------------------------------
1403 // DRAG & RESIZE
1404 // --------------------------------------------------
1405
1406 initDrag(windowId) {
1407 const windowData = this.windows.get(windowId);
1408 if (!windowData || windowData.cfg.draggable == 0) return;
1409
1410 if (this.isMobileViewport()) return;
1411
1412 const $win = windowData.element;
1413 const $header = $win.find(".dbx-window-header");
1414
1415 $header.css("cursor", "move");
1416
1417 $header.off('mousedown.drag').on('mousedown.drag', (e) => {
1418 if (!$(e.target).closest('.dbx-window-header').length) return;
1419
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) {
1421 return;
1422 }
1423
1424 this.bringToFront(windowId);
1425
1426 // 🔥 NEU: dragging class
1427 $win.addClass("dbx-window-dragging");
1428
1429 this.activeDrag = {
1430 windowId: windowId,
1431 startX: e.clientX,
1432 startY: e.clientY,
1433 startLeft: parseFloat($win.css('left')),
1434 startTop: parseFloat($win.css('top'))
1435 };
1436
1437 e.preventDefault();
1438 });
1439
1440 $header.off('touchstart.drag').on('touchstart.drag', (e) => {
1441 if (this.isMobileViewport()) return;
1442
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) {
1444 return;
1445 }
1446
1447 const touch = e.originalEvent?.touches?.[0];
1448 if (!touch) return;
1449
1450 this.bringToFront(windowId);
1451
1452 // 🔥 NEU
1453 $win.addClass("dbx-window-dragging");
1454
1455 this.activeDrag = {
1456 windowId: windowId,
1457 startX: touch.clientX,
1458 startY: touch.clientY,
1459 startLeft: parseFloat($win.css('left')),
1460 startTop: parseFloat($win.css('top'))
1461 };
1462
1463 e.preventDefault();
1464 });
1465 }
1466
1467 updateDragPosition(e) {
1468 if (!this.activeDrag) return;
1469
1470 const windowData = this.windows.get(this.activeDrag.windowId);
1471 if (!windowData) return;
1472
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;
1476
1477 const dx = clientX - this.activeDrag.startX;
1478 const dy = clientY - this.activeDrag.startY;
1479
1480 let newLeft = this.activeDrag.startLeft + dx;
1481 let newTop = this.activeDrag.startTop + dy;
1482
1483 const maxLeft = window.innerWidth - windowData.element.outerWidth();
1484 const maxTop = window.innerHeight - windowData.element.outerHeight();
1485
1486 newLeft = Math.min(Math.max(newLeft, 0), maxLeft);
1487 newTop = Math.min(Math.max(newTop, 0), maxTop);
1488
1489 windowData.element.css({
1490 left: newLeft + 'px',
1491 top: newTop + 'px'
1492 });
1493 }
1494
1495 initResize(windowId) {
1496 const windowData = this.windows.get(windowId);
1497 if (!windowData || windowData.cfg.resizable == 0) return;
1498
1499 if (this.isMobileViewport()) return;
1500
1501 const $win = windowData.element;
1502 const $resize = $win.find(".dbx-window-resize");
1503
1504 $resize.off('mousedown.resize').on('mousedown.resize', (e) => {
1505 this.bringToFront(windowId);
1506
1507 // 🔥 NEU
1508 $win.addClass("dbx-window-resizing");
1509
1510 this.activeResize = {
1511 windowId: windowId,
1512 startX: e.clientX,
1513 startY: e.clientY,
1514 startWidth: $win.outerWidth(),
1515 startHeight: $win.outerHeight()
1516 };
1517
1518 e.preventDefault();
1519 e.stopPropagation();
1520 });
1521
1522 $resize.off('touchstart.resize').on('touchstart.resize', (e) => {
1523 if (this.isMobileViewport()) return;
1524
1525 const touch = e.originalEvent?.touches?.[0];
1526 if (!touch) return;
1527
1528 this.bringToFront(windowId);
1529
1530 // 🔥 NEU
1531 $win.addClass("dbx-window-resizing");
1532
1533 this.activeResize = {
1534 windowId: windowId,
1535 startX: touch.clientX,
1536 startY: touch.clientY,
1537 startWidth: $win.outerWidth(),
1538 startHeight: $win.outerHeight()
1539 };
1540
1541 e.preventDefault();
1542 e.stopPropagation();
1543 });
1544 }
1545
1546 updateResizeSize(e) {
1547 if (!this.activeResize) return;
1548
1549 const windowData = this.windows.get(this.activeResize.windowId);
1550 if (!windowData) return;
1551
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;
1555
1556 const dx = clientX - this.activeResize.startX;
1557 const dy = clientY - this.activeResize.startY;
1558
1559 let newWidth = this.activeResize.startWidth + dx;
1560 let newHeight = this.activeResize.startHeight + dy;
1561
1562 const viewportWidth = window.innerWidth;
1563 const viewportHeight = window.innerHeight;
1564
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);
1569
1570 newWidth = Math.min(Math.max(newWidth, minWidth), maxWidth);
1571 newHeight = Math.min(Math.max(newHeight, minHeight), maxHeight);
1572
1573 windowData.element.css({
1574 width: newWidth + 'px',
1575 height: newHeight + 'px'
1576 });
1577 }
1578
1579 // --------------------------------------------------
1580 // MINIMIZE / MAXIMIZE
1581 // --------------------------------------------------
1582
1583 initMinimize(windowId) {
1584 const windowData = this.windows.get(windowId);
1585 if (!windowData) return;
1586 if (this.isMobileViewport()) return;
1587
1588 const $win = windowData.element;
1589 const $minimizeBtn = $win.find(".dbx-window-minimize");
1590
1591 if (!$minimizeBtn.length) return;
1592
1593 $minimizeBtn.off('click.minimize').on('click.minimize', () => {
1594 if (windowData.minimized) {
1595 this.restoreWindow(windowId);
1596 } else {
1597 this.minimizeWindow(windowId);
1598 }
1599 });
1600 }
1601
1602 minimizeWindow(windowId) {
1603 const windowData = this.windows.get(windowId);
1604 if (!windowData || windowData.minimized) return;
1605 if (this.isMobileViewport()) return;
1606
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);
1612
1613 this.bringToFront(windowId);
1614 $dockItem.addClass("is-minimizing");
1615
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");
1622
1623 if (windowData.overlay) {
1624 windowData.overlay.hide();
1625 }
1626
1627 this.activateTopVisibleWindow(windowId);
1628 this.scheduleSaveState();
1629 });
1630 }
1631
1632 initMaximize(windowId) {
1633 const windowData = this.windows.get(windowId);
1634 if (!windowData) return;
1635 if (this.isMobileViewport()) return;
1636
1637 const $win = windowData.element;
1638 const $maximizeBtn = $win.find(".dbx-window-maximize");
1639
1640 if (!$maximizeBtn.length) return;
1641
1642 $maximizeBtn.off('click.maximize').on('click.maximize', () => {
1643
1644 // 🔥 FULLSCREEN MODE
1645 if (windowData.fullscreen) {
1646
1647 if (windowData.originalSize) {
1648 $win.css({
1649 width: windowData.originalSize.width,
1650 height: windowData.originalSize.height,
1651 left: windowData.originalPosition.left,
1652 top: windowData.originalPosition.top
1653 });
1654 }
1655
1656 windowData.fullscreen = false;
1657 $win.removeClass("dbx-window-fullscreen");
1658
1659 $maximizeBtn.html('<i class="bi bi-square"></i>');
1660
1661 } else {
1662
1663 windowData.originalSize = {
1664 width: $win.css('width'),
1665 height: $win.css('height')
1666 };
1667
1668 windowData.originalPosition = {
1669 left: $win.css('left'),
1670 top: $win.css('top')
1671 };
1672
1673 $win.css({
1674 width: '100%',
1675 height: '100%',
1676 left: '0',
1677 top: '0'
1678 });
1679
1680 windowData.fullscreen = true;
1681 $win.addClass("dbx-window-fullscreen");
1682
1683 $maximizeBtn.html('<i class="bi bi-arrows-angle-contract"></i>');
1684 }
1685
1686 this.scheduleSaveState();
1687 });
1688 }
1689
1690 // --------------------------------------------------
1691 // CONTENT LOADING
1692 // --------------------------------------------------
1693
1694 loadContent(windowId, url, cfg) {
1695 const windowData = this.windows.get(windowId);
1696 if (!windowData) return;
1697
1698 const $body = windowData.element.find(".dbx-window-body");
1699 const $target = this.getWindowInnerTarget(windowData);
1700 this.syncContentFlags(windowData);
1701
1702 $target.html(`
1703 <div class="dbx-window-loading">
1704 Laden...
1705 </div>
1706 `);
1707
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>
1714 </div>`;
1715 $target.html(errorMsg);
1716 this.triggerCallback(cfg.onError, windowData.element[0], "ajax.js nicht geladen.");
1717 return;
1718 }
1719
1720 const requestConfig = {
1721 url: url,
1722 method: cfg.method || "GET",
1723 mode: "html",
1724 data: cfg.data || {},
1725 timeout: cfg.timeout || 30000
1726 };
1727
1728 if (cfg.headers && typeof cfg.headers == 'object') {
1729 requestConfig.headers = cfg.headers;
1730 }
1731
1732 dbx.ajax.request(requestConfig)
1733 .then(response => {
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);
1739
1740 this.rescanContent($target[0]);
1741 if (windowData.restoreState && windowData.restoreState.scrollTop) {
1742 $body.scrollTop(windowData.restoreState.scrollTop);
1743 }
1744 this.triggerCallback(cfg.onLoad, windowData.element[0]);
1745 })
1746 .catch(error => {
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>
1754 </div>`;
1755 $target.html(errorMsg);
1756 this.triggerCallback(cfg.onError, windowData.element[0], error);
1757 });
1758 }
1759
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);
1768 }
1769 return $target;
1770 }
1771
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);
1776 }
1777
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);
1785 cfg.title = title;
1786 windowData.element.find(".dbx-window-title").html(title);
1787 }
1788 if (cfg.content !== undefined) {
1789 $target.empty().append(cfg.content);
1790 } else if (cfg.html !== undefined) {
1791 $target.html(cfg.html);
1792 } else {
1793 return null;
1794 }
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);
1800 return windowId;
1801 }
1802
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;
1809
1810 if (cfg.content !== undefined || cfg.html !== undefined) {
1811 return this.setWindowContent(windowId, cfg);
1812 }
1813
1814 if (!url) return null;
1815 if (cfg.ajax != 0) {
1816 url = this.appendAjaxFlag(url);
1817 }
1818 this.loadContent(windowId, url, cfg);
1819 this.bringToFront(windowId);
1820 return windowId;
1821 }
1822
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') {
1828 dbx.scan(element);
1829 }
1830
1831 if (dbx.iconsEditor && typeof dbx.iconsEditor.rescan == 'function') {
1832 dbx.iconsEditor.rescan(element);
1833 }
1834
1835 // Sonst nichts tun
1836 }
1837
1838 // --------------------------------------------------
1839 // OVERLAY
1840 // --------------------------------------------------
1841
1842 createOverlay(windowId) {
1843 const overlayId = `dbx_overlay_${windowId}`;
1844
1845 const windowData = this.windows.get(windowId);
1846 if (!windowData) return;
1847
1848 const winZ = parseInt(windowData.element.css('z-index'), 10) || CONFIG.BASE_Z_MODAL;
1849 const zIndex = winZ - 1;
1850
1851 const $overlay = $(`
1852 <div id="${overlayId}" class="dbx-window-overlay"
1853 style="position:fixed; top:0; left:0; width:100%; height:100%;
1854 z-index:${zIndex};
1855 display:none;">
1856 </div>
1857 `);
1858
1859 $("body").append($overlay);
1860 $overlay.fadeIn(CONFIG.ANIMATION_DURATION);
1861
1862 if (windowData.cfg.clickToClose == 1) {
1863 $overlay.on('click', () => {
1864 this.closeWindow(windowId);
1865 });
1866 }
1867
1868 windowData.overlay = $overlay;
1869 }
1870
1871 // --------------------------------------------------
1872 // FENSTER ERSTELLEN
1873 // --------------------------------------------------
1874
1875 createWindow(cfg, url, callerEl = null) {
1876 cfg = $.extend(true, {}, cfg || {});
1877 cfg = this.normalizeConfig(cfg);
1878
1879 const restoreState = cfg.__restoreState || null;
1880 const isRestore = cfg.__restore == 1 || cfg.restore == 1;
1881 const restoreMinimized = cfg.__restoreMinimized == 1;
1882
1883 delete cfg.__restoreState;
1884 delete cfg.__restore;
1885 delete cfg.__restoreMinimized;
1886 delete cfg.restore;
1887
1888 if (this.stack.length >= CONFIG.MAX_WINDOWS) {
1889 this.safeWarn("maximum windows reached", CONFIG.MAX_WINDOWS);
1890 this.triggerCallback(cfg.onMaxReached);
1891 return null;
1892 }
1893
1894 this.counter++;
1895 const windowId = `dbxWindow_${this.counter}_${Date.now()}`;
1896
1897 const isModal = (cfg.modal == 1 || cfg.mode == "modal-fullscreen");
1898 const zIndex = this.getNextZIndex(isModal, cfg, callerEl);
1899
1900 const title = this.decodeConfigText(cfg.title || 'Fenster');
1901 cfg.title = title;
1902 const footer = cfg.footer || '';
1903 const isMobileViewport = this.isMobileViewport();
1904
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);
1909
1910 const scrollClass = (cfg.scroll == 0) ? 'dbx-window-no-scroll' : 'dbx-window-scroll';
1911
1912 const customTools = isMobileViewport ? '' : (cfg.tools || '');
1913
1914 const html = `
1915 <div class="dbx-window ${isModal ? 'dbx-window-modal' : 'dbx-window-draggable'} ${scrollClass}"
1916 id="${windowId}"
1917 style="z-index:${zIndex}; position:absolute; display:none;"
1918 tabindex="-1"
1919 role="dialog"
1920 aria-label="">
1921
1922 <div class="dbx-window-header">
1923 <span class="dbx-window-title">${title}</span>
1924
1925 <div class="dbx-window-controls">
1926
1927 ${customTools}
1928
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>' : ''}
1933 </div>
1934 </div>
1935
1936 <div class="dbx-window-body"><div class="dbx-window-inner" data-openwin-inner id="inner_${windowId}"></div></div>
1937
1938 ${footer ? `<div class="dbx-window-footer">${footer}</div>` : ''}
1939
1940 ${!isMobileViewport && cfg.resizable != 0 ? '<div class="dbx-window-resize"></div>' : ''}
1941 </div>
1942 `;
1943
1944 $("body").append(html);
1945 const $win = $(`#${windowId}`);
1946
1947 const windowData = {
1948 id: windowId,
1949 element: $win,
1950 cfg: cfg,
1951 url: url,
1952 fingerprint: this.getWindowFingerprint(cfg, url),
1953 callerEl: callerEl,
1954 isModal: isModal,
1955 minimized: false,
1956 maximized: false,
1957 fullscreen: false,
1958 originalSize: null,
1959 originalPosition: null,
1960 overlay: null,
1961 restoreState: restoreState,
1962 selectedValues: []
1963 };
1964
1965 this.windows.set(windowId, windowData);
1966 this.stack.push(windowData);
1967
1968 this.calculateWindowSize(windowData, cfg);
1969 if (restoreState) {
1970 this.applySavedWindowState(windowData, restoreState);
1971 }
1972
1973 if (!isRestore && cfg.mode === "prompt" && callerEl && !isMobileViewport) {
1974
1975 const rect = callerEl.getBoundingClientRect();
1976 const viewportWidth = window.innerWidth;
1977 const viewportHeight = window.innerHeight;
1978
1979 let left = rect.left + window.scrollX;
1980 let top = rect.bottom + window.scrollY;
1981
1982 let width = $win.outerWidth();
1983 let height = $win.outerHeight();
1984
1985 if (!cfg.width) {
1986 width = Math.min(300, viewportWidth - 20);
1987 $win.css("width", width + "px");
1988 }
1989
1990 if (!cfg.height) {
1991 $win.css("height", "auto");
1992 height = $win.outerHeight();
1993 }
1994
1995 if (left + width > viewportWidth + window.scrollX) {
1996 left = viewportWidth + window.scrollX - width - 10;
1997 }
1998
1999 if (top + height > viewportHeight + window.scrollY) {
2000 top = rect.top + window.scrollY - height;
2001 }
2002
2003 left = Math.max(10, left);
2004 top = Math.max(10, top);
2005
2006 $win.css({
2007 left: left,
2008 top: top
2009 });
2010 }
2011
2012 // 🔥 PROMPT: OUTSIDE CLICK CLOSE
2013 if (!isRestore && cfg.mode === "prompt") {
2014
2015 setTimeout(() => {
2016 $(document).on(`mousedown.${windowId}`, (ev) => {
2017
2018 const $target = $(ev.target);
2019
2020 if (
2021 !$target.closest(`#${windowId}`).length &&
2022 (!callerEl || (!$target.is(callerEl) && !$target.closest(callerEl).length))
2023 ) {
2024
2025 this.closeWindow(windowId);
2026 }
2027
2028 });
2029 }, 0);
2030 }
2031
2032 if (
2033 !restoreState &&
2034 (
2035 cfg.mode == "fullscreen" ||
2036 cfg.mode == "modal-fullscreen" ||
2037 cfg.position == "fullscreen"
2038 )
2039 ) {
2040 $win.css({
2041 width: '100%',
2042 height: '100%',
2043 left: '0',
2044 top: '0'
2045 });
2046
2047 windowData.fullscreen = true;
2048 $win.addClass("dbx-window-fullscreen");
2049
2050 $win.find(".dbx-window-maximize")
2051 .html('<i class="bi bi-arrows-angle-contract"></i>');
2052 }
2053
2054 if (isMobileViewport) {
2055 this.applyMobileWindowMode(windowData);
2056 }
2057
2058 this.initWindowEvents(windowId, cfg);
2059
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);
2065
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);
2070 } else {
2071 $body.html(cfg.html);
2072 }
2073 this.rescanContent($body[0]);
2074 this.triggerCallback(cfg.onLoad, $win[0]);
2075 } else {
2076 this.loadContent(windowId, url, cfg);
2077 }
2078
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();
2086 }
2087 } else {
2088 $win.css("display", "flex");
2089 if (isMobileViewport) {
2090 this.applyMobileWindowMode(windowData);
2091 }
2092 this.bringToFront(windowId);
2093
2094 if (!isRestore) {
2095 this.animateElement($win, this.getAnimationName(cfg, "open"), cfg);
2096
2097 const $firstInput = $win.find("input,textarea,select").first();
2098 if ($firstInput.length) {
2099 $firstInput.focus();
2100 } else {
2101 $win.focus();
2102 }
2103 }
2104 }
2105
2106 if (!isRestore) {
2107 this.triggerCallback(cfg.onOpen, $win[0]);
2108 this.scheduleSaveState();
2109 }
2110
2111 this.safeLog("window created", windowId, "modal:", isModal, "z-index:", zIndex);
2112 this.updateDockUi();
2113
2114 return windowId;
2115 }
2116
2117 calculateWindowSize(windowData, cfg) {
2118 const $win = windowData.element;
2119 const el = $win[0];
2120
2121 const viewportWidth = window.innerWidth;
2122 const viewportHeight = window.innerHeight;
2123
2124 let width = this.parseSize(cfg.width, viewportWidth);
2125 let height = this.parseSize(cfg.height, viewportHeight);
2126
2127 if (width == null || width == "auto") {
2128 width = Math.round(viewportWidth * CONFIG.DEFAULT_WIDTH);
2129 }
2130 if (height == null || height == "auto") {
2131 height = Math.round(viewportHeight * CONFIG.DEFAULT_HEIGHT);
2132 }
2133
2134 const minWidth = this.parseSize(cfg.minWidth, viewportWidth, CONFIG.MIN_WIDTH);
2135 const maxWidth = this.parseSize(cfg.maxWidth, viewportWidth, viewportWidth);
2136
2137 // 🔥 FIX: minHeight prüfen
2138 let minHeight;
2139 if (cfg.minHeight == undefined || cfg.minHeight == null) {
2140 minHeight = 200;
2141 } else {
2142 minHeight = this.parseSize(cfg.minHeight, viewportHeight, CONFIG.MIN_HEIGHT);
2143 }
2144
2145 const maxHeight = this.parseSize(cfg.maxHeight, viewportHeight, viewportHeight);
2146
2147 width = Math.min(Math.max(width, minWidth), maxWidth);
2148 height = Math.min(Math.max(height, minHeight), maxHeight);
2149
2150 let left, top;
2151
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);
2161 } else {
2162 const offset = Math.min(this.stack.length * CONFIG.DRAG_OFFSET, 200);
2163 left = 100 + offset;
2164 top = 100 + offset;
2165 }
2166
2167 left = Math.min(Math.max(left, 0), viewportWidth - 50);
2168 top = Math.min(Math.max(top, 0), viewportHeight - 50);
2169
2170 $win.css({
2171 width: width + 'px',
2172 height: height + 'px',
2173 left: left + 'px',
2174 top: top + 'px'
2175 });
2176
2177 // 🔥 WICHTIG: !important setzen
2178 if (cfg.minHeight == undefined || cfg.minHeight == null) {
2179 el.style.setProperty('min-height', '200px', 'important');
2180 }
2181
2182 if (this.isMobileViewport()) {
2183 this.applyMobileWindowMode(windowData);
2184 }
2185 }
2186
2187 initWindowEvents(windowId, cfg) {
2188 const windowData = this.windows.get(windowId);
2189 if (!windowData) return;
2190
2191 const $win = windowData.element;
2192
2193 $win.off('mousedown.front').on('mousedown.front', () => {
2194 this.bringToFront(windowId);
2195 });
2196
2197 $win.find(".dbx-window-close")
2198 .off('click.close touchend.close')
2199 .on('click.close touchend.close', (e) => {
2200 e.preventDefault();
2201 e.stopPropagation();
2202 this.closeWindow(windowId);
2203 });
2204
2205 $win.attr('tabindex', '-1');
2206 }
2207
2208 // --------------------------------------------------
2209 // FENSTER SCHLIESSEN
2210 // --------------------------------------------------
2211
2212 closeWindow(windowId) {
2213 const windowData = this.windows.get(windowId);
2214 if (!windowData) return;
2215
2216 const $win = windowData.element;
2217 const cfg = windowData.cfg;
2218
2219 this.safeLog("close", windowId);
2220
2221 let shouldClose = true;
2222 if (cfg.onBeforeClose) {
2223 const result = this.triggerCallbackWithReturn(cfg.onBeforeClose, $win[0]);
2224 if (result == false) shouldClose = false;
2225 }
2226
2227 if (!shouldClose) return;
2228
2229 windowData.closing = true;
2230 this.removeDockItem(windowId);
2231
2232 if (windowData.overlay) {
2233 windowData.overlay.fadeOut(CONFIG.ANIMATION_DURATION, () => {
2234 windowData.overlay.remove();
2235 });
2236 }
2237
2238 this.animateElement($win, this.getAnimationName(cfg, "close"), cfg, () => {
2239 $win.hide();
2240 $(document).off(`.${windowId}`);
2241 $win.off();
2242 $win.remove();
2243
2244 const idx = this.stack.findIndex(w => w.id == windowId);
2245 if (idx != -1) this.stack.splice(idx, 1);
2246
2247 this.windows.delete(windowId);
2248 this.updateAllZIndices();
2249 this.updateDockUi();
2250 this.scheduleSaveState();
2251
2252 // --------------------------------------------------
2253 // 🔥 NEU: AFTER CLOSE ACTIONS
2254 // --------------------------------------------------
2255
2256 if (cfg.afterClose) {
2257
2258 this.safeLog("afterClose", cfg.afterClose);
2259
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);
2264 }
2265 }
2266
2267 // reload alle fenster
2268 if (cfg.afterClose === "reloadAll") {
2269 this.getAllWindows().forEach(w => {
2270 if (w && w.id) this.reloadWindow(w.id);
2271 });
2272 }
2273
2274 // custom callback
2275 if (typeof cfg.afterClose === "string" && window[cfg.afterClose]) {
2276 window[cfg.afterClose]();
2277 }
2278
2279 if (typeof cfg.afterClose === "function") {
2280 cfg.afterClose();
2281 }
2282 }
2283
2284 this.triggerCallback(cfg.onClose);
2285 this.safeLog("closed", windowId);
2286 });
2287 }
2288
2289 resolve(windowId, value) {
2290 const windowData = this.windows.get(windowId);
2291 if (!windowData) return;
2292
2293 const cfg = windowData.cfg;
2294 const callerEl = windowData.callerEl;
2295
2296 this.safeLog("resolve", windowId, value);
2297
2298 // 🔥 NORMALIZE
2299 let outValue = value;
2300
2301 if (Array.isArray(value)) {
2302 outValue = value.map(v => v.value).join(",");
2303 } else if (value && typeof value === "object") {
2304 outValue = value.value;
2305 }
2306
2307 // 🔥 PROMPT MODE
2308 if (cfg.mode === "prompt") {
2309 if (callerEl) {
2310 if ("value" in callerEl) {
2311 callerEl.value = outValue;
2312 }
2313 $(callerEl).trigger("change");
2314 }
2315
2316 if (cfg.keepOpen != 1) {
2317 this.closeWindow(windowId);
2318 }
2319
2320 return;
2321 }
2322
2323 // 🔥 EVENT SYSTEM
2324 if (cfg.event) {
2325
2326 let name = cfg.event;
2327
2328 if (cfg.target && cfg.target !== "broadcast") {
2329 name = cfg.target + ":" + cfg.event;
2330 }
2331
2332 dbx.event.emit(name, value);
2333 }
2334 }
2335
2336 // --------------------------------------------------
2337 // ÖFFENTLICHE API
2338 // --------------------------------------------------
2339
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);
2345 }
2346
2347 let url = cfg.url || (el ? el.getAttribute("href") : null);
2348 if (!url && (cfg.content !== undefined || cfg.html !== undefined)) {
2349 url = "about:blank";
2350 cfg.ajax = 0;
2351 cfg.persist = 0;
2352 cfg.reloadable = 0;
2353 cfg.reload = 0;
2354 }
2355 if (!url) {
2356 this.safeWarn("no url");
2357 return null;
2358 }
2359
2360 const mode = (cfg.mode || "panel").toLowerCase();
2361
2362 if (String(cfg.target || "").toLowerCase() == "self") {
2363 const replaced = this.replaceSelfWindowContent(el, cfg, url);
2364 if (replaced) return replaced;
2365 }
2366
2367 if (mode == "popup") {
2368 return this.openPopup(url, cfg);
2369 }
2370
2371 if (cfg.ajax != 0) {
2372 url = this.appendAjaxFlag(url);
2373 }
2374
2375 if (cfg.reuse != 0 && cfg.allowDuplicate != 1) {
2376 const fingerprint = this.getWindowFingerprint(cfg, url);
2377 const existing = this.findReusableWindow(fingerprint);
2378
2379 if (existing) {
2380 this.safeLog("reuse window", existing.id, url);
2381 this.restoreWindow(existing.id);
2382 this.triggerCallback(cfg.onReuse, existing.element[0]);
2383 return existing.id;
2384 }
2385 }
2386
2387 return this.createWindow(cfg, url, el); // 🔥 NEU
2388 }
2389
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;
2395
2396 const features = [
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'
2402 ].join(',');
2403
2404 const winName = cfg.windowName || `dbx_popup_${Date.now()}`;
2405 const win = window.open(url, winName, features);
2406
2407 if (!win && cfg.fallback != 0) {
2408 if (cfg.fallback == 'redirect') {
2409 window.location.href = url;
2410 } else {
2411 window.open(url, '_blank');
2412 }
2413 }
2414
2415 win && win.focus();
2416 return win;
2417 }
2418
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; }
2424
2425 }
2426
2427 // --------------------------------------------------
2428 // GLOBALE INITIALISIERUNG
2429 // --------------------------------------------------
2430
2431 function registerOpenWinFeature(windowManager) {
2432 if (!windowManager || !dbx.feature || !dbx.feature.register) return;
2433 if (dbx.feature.has && dbx.feature.has("openWin")) return;
2434
2435 dbx.feature.register("openWin", {
2436 version: "2.1.0",
2437
2438 scope: "global",
2439
2440 css: [
2441 ['css', 'design', 'c-openWin.css']
2442 ],
2443
2444 js: [
2445 ['js', 'lib', 'ajax.js']
2446 ],
2447
2448 init: (el, cfg) => {
2449
2450 el.__dbxInitialized = el.__dbxInitialized || {};
2451 if (el.__dbxInitialized["openWin"]) return;
2452
2453 windowManager.restoreState();
2454 }
2455 });
2456 }
2457
2458 if (!dbx.__openWinManager) {
2459
2460 const windowManager = new WindowManager();
2461 dbx.__openWinManagerInstance = windowManager;
2462
2463 // Öffentliche API
2464 dbx.openWin = {
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
2478 };
2479
2480 // Feature registrieren
2481 registerOpenWinFeature(windowManager);
2482
2483 setTimeout(() => windowManager.restoreState(), 120);
2484
2485 dbx.__openWinManager = true;
2486
2487 windowManager.safeLog("ready - modal z-index:", CONFIG.BASE_Z_MODAL,
2488 "dragable z-index:", CONFIG.BASE_Z_DRAGGABLE);
2489 }
2490
2491 registerOpenWinFeature(dbx.__openWinManagerInstance);
2492 setTimeout(() => registerOpenWinFeature(dbx.__openWinManagerInstance), 0);
2493 setTimeout(() => registerOpenWinFeature(dbx.__openWinManagerInstance), 80);
2494
2495 if (dbx.declare && typeof dbx.declare.registerSchema === "function") {
2496
2497 dbx.declare.registerSchema("openWin", {
2498 fields: {
2499 url: {
2500 default: "",
2501 aliases: ["data-url"],
2502 infer: function (el) {
2503 return el && el.getAttribute ? (el.getAttribute("href") || "") : "";
2504 }
2505 },
2506 title: {
2507 default: "Fenster",
2508 aliases: ["data-title"],
2509 infer: function (el) {
2510 return el && el.getAttribute ? (el.getAttribute("title") || "") : "";
2511 }
2512 },
2513 height: {
2514 default: "760",
2515 aliases: ["data-height", "data-size-y"]
2516 },
2517 width: {
2518 default: "1280",
2519 aliases: ["data-width", "data-size-x"]
2520 },
2521 sizeX: {
2522 default: "",
2523 aliases: ["data-size-x"]
2524 },
2525 sizeY: {
2526 default: "",
2527 aliases: ["data-size-y"]
2528 },
2529 modal: {
2530 default: "0",
2531 aliases: ["data-modal"]
2532 },
2533 scroll: {
2534 default: "1",
2535 aliases: ["data-scroll"]
2536 },
2537 position: {
2538 default: "center",
2539 aliases: ["data-position"]
2540 },
2541 minimizable: {
2542 default: "1",
2543 aliases: ["data-minimizable"]
2544 },
2545 maximizable: {
2546 default: "1",
2547 aliases: ["data-maximizable"]
2548 },
2549 resizable: {
2550 default: "1",
2551 aliases: ["data-resizable"]
2552 }
2553 }
2554 });
2555
2556 dbx.declare.transforms.openWin = function (raw) {
2557 const width = raw.width || raw.sizeX || "1280";
2558 const height = raw.height || raw.sizeY || "760";
2559 return {
2560 lib: "openWin",
2561 url: String(raw.url || "").trim(),
2562 title: String(raw.title || "Fenster").trim(),
2563 height: height,
2564 width: width,
2565 sizeX: raw.sizeX || width,
2566 sizeY: raw.sizeY || height,
2567 modal: raw.modal,
2568 scroll: raw.scroll,
2569 position: String(raw.position || "center").trim(),
2570 left: raw.left,
2571 top: raw.top,
2572 resizable: raw.resizable,
2573 minimizable: raw.minimizable,
2574 maximizable: raw.maximizable
2575 };
2576 };
2577 }
2578
2579})(window, document, jQuery);