1/** 2 * modal_layout.js 3 * 4 * Fixes modal layout at all zoom levels. 5 * 6 * Problems solved: 7 * [4] Modal sits too low â Bootstrap sets .modal padding-top ~86px which pushes 8 * the dialog below the navbar. We override with the actual navbar height. 9 * [5] Col3 readme content doesn't scroll â the flex chain from modal-body â row â 10 * col â inner divs â readme-content fails. 11 * [6] At zoom >100%, same problem is worse. 12 * [7] Small modals (change-pwd, login, confirm...) bá» full-height giá»ng large modals 13 * do fixModalPosition() inject height inline style cho tất cả. 14 * [8] admin-modal khi á» login state vẫn chiếm 95vw â cần thu nhá» lại. 15 * [9] Click outside modal to close â for simple modals close immediately; 16 * for admin-modal trigger the close-btn which checks unsaved changes. 17 */ 18 19(function () { 20 'use strict'; 21 22 // ââ Config ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 23 // Chá» các modal nà y má»i cần full-height (chiếm gần hết viewport) 24 var LARGE_MODAL_IDS = [ 25 'about-modal', 26 'admin-modal', 27 'user-logs-modal', 28 'layout-editor-modal', 29 'batch-export-modal', 30 'maint-cl-detail-modal', 31 'incident-report-modal', 32 'rootcause-modal', 33 'maint-sp-mini-modal', 34 'incident-spare-modal', 35 ]; 36 37 // Các modal AUTO-HEIGHT: shrink vừa ná»i dung khi Ãt, expand tá»i max-height khi nhiá»u 38 // Không bao giá» bá» fixed height như LARGE_MODAL_IDS 39 // NOTE: drilldown-modal Äã ÄÆ°á»£c xá» lý bá»i applyAutoHeightXlModalStyle() trong utils_js.py 40 // (detect bằng #drilldown-modal-body) â không ÄÆ°a và o Äây Äá» tránh conflict. 41 var AUTO_HEIGHT_MODAL_IDS = [ 42 // Các modal dưá»i Äây ÄÆ°á»£c xá» lý bá»i utils_js.py applyAutoHeightXlModalStyle 43 // (detect bằng unique child ID, scale-aware) â KHÃNG ÄÆ°a và o Äây Äá» tránh conflict: 44 // 'drilldown-modal', 'maint-settings-modal', 'planning-planned-status-modal', 'asset-detail-modal' 45 'sp-add-modal', // Modal Thêm váºt tư má»i 46 'sp-in-modal', // Modal Nháºp kho 47 'sp-out-modal', // Modal Xuất kho 48 'online-users-modal', // Modal Ngưá»i dùng Online 49 'batch-export-modal-auto', // (alias, handled below) 50 // (Fix modal Shift Schedule bá» top quá thấp / không co giãn theo 51 // % viewport / không scroll ÄÆ°á»£c ná»i dung dà i trên các mà n hình 52 // khác resolution â vd 14" vs 24") â áp dụng cùng cÆ¡ chế 53 // auto-height Äang dùng á»n Äá»nh cho sp-add-modal/sp-in-modal. 54 'shift-add-code-modal', // Modal "Thêm mã ca hoặc mã nghá»" 55 'shift-add-employee-modal', // Modal "Thêm/Sá»a nhân viên" 56 'shift-schedule-guide-modal', // Modal "Hưá»ng dẫn xếp ca" 57 'role-edit-modal', // Modal "Edit Role Permissions" (Admin Settings > Users/Roles) 58 // (FIX <wp-edit-modal-scroll-y>: modal Sá»a Work Permit không có 59 // scroll Y â ná»i dung dà i (nhiá»u field) bá» trà n xuá»ng dưá»i viewport 60 // và không Äá»c/thao tác ÄÆ°á»£c) â áp dụng cùng cÆ¡ chế auto-height 61 // Äang dùng á»n Äá»nh cho role-edit-modal (max-height theo viewport, 62 // modal-body tá»± scroll khi ná»i dung dà i). 63 'wp-edit-modal', // Modal "Sá»a Work Permit" (tab Work Permit) 64 ]; 65 66 // Các modal nhá» â CHá» fix margin-top (Äá» clear navbar), KHÃNG set height 67 var SMALL_MODAL_IDS = [ 68 'change-pwd-modal', 69 'login-modal', 70 'confirm-delete-modal', 71 'confirm-save-modal', 72 'admin-confirm-modal', 73 'admin-action-confirm-modal', 74 'fullscreen-modal', 75 'maint-settings-confirm-modal', 76 ]; 77 78 var ALL_MODAL_IDS = LARGE_MODAL_IDS.concat(AUTO_HEIGHT_MODAL_IDS).concat(SMALL_MODAL_IDS); 79 80 // Fix #9: Modals that should close when clicking the backdrop 81 // For admin-modal: clicking outside triggers the close-btn (which checks unsaved changes) 82 // For layout-editor-modal: clicking outside triggers the layout close button 83 // Simple modals: click outside â click the close/X button automatically 84 var BACKDROP_CLOSE_CONFIG = { 85 // modal_id: close_btn_id or 'default' (use built-in X close button)
86 'session-details-modal': 'session-details-modal-close-btn', 87 'online-users-modal': 'online-users-modal-close-btn', 88 'drilldown-modal': 'drilldown-modal-close', 89 'sync-logs-modal': 'sync-logs-close-btn', 90 'about-modal': null, // Use built-in close button (close_button=True) 91 'admin-modal': 'admin-modal-close-btn', // Triggers unsaved-changes check 92 'layout-editor-modal': 'layout-editor-close-btn', // Triggers unsaved-changes check 93 'user-logs-modal': 'user-logs-close-btn', 94 'batch-export-modal': null, // batch-export: keep static (complex state) 95 }; 96 97 // ââ Navbar height helper âââââââââââââââââââââââââââââââââââââââââââââââââ 98 function getNavbarHeight() { 99 var nav = document.querySelector( 100 'nav.navbar, .navbar, header[class*="navbar"], #navbar' 101 ); 102 if (nav) return nav.getBoundingClientRect().height; 103 var v = getComputedStyle(document.documentElement) 104 .getPropertyValue('--navbar-height'); 105 if (v) return parseFloat(v) || 66; 106 return 66; 107 } 108 109 // ââ Kiá»m tra admin-modal Äang á» trạng thái login hay settings ââââââââââââ 110 function isAdminLoginState() { 111 var loginSection = document.getElementById('admin-login-section'); 112 var settingsSection = document.getElementById('admin-settings-section'); 113 if (!loginSection || !settingsSection) return false; 114 var loginVisible = (loginSection.style.display !== 'none') && 115 (getComputedStyle(loginSection).display !== 'none'); 116 var settingsVisible = (settingsSection.style.display !== 'none') && 117 (getComputedStyle(settingsSection).display !== 'none'); 118 return loginVisible && !settingsVisible; 119 } 120 121 // ââ Toggle class trên admin-modal Äá» CSS có thá» target âââââââââââââââââââ 122 function updateAdminModalState() { 123 var adminModal = document.getElementById('admin-modal'); 124 if (!adminModal) return; 125 if (isAdminLoginState()) { 126 adminModal.classList.add('admin-modal--login-state'); 127 } else { 128 adminModal.classList.remove('admin-modal--login-state'); 129 } 130 } 131 132 // ââ Fix large modal: full-height, just below navbar ââââââââââââââââââââââ 133 function fixLargeModalPosition(modalEl, modalId) { 134 var navH = getNavbarHeight(); 135 // Dùng gap nhá» hÆ¡n trên mà n có viewport thấp (14" thưá»ng < 800px) 136 // Äá» modal expand gần tá»i bottom hÆ¡n 137 var gap = window.innerHeight < 800 ? 2 : 6; 138 139 modalEl.style.paddingTop = '0px'; 140 modalEl.style.paddingBottom = '0px'; 141 142 var dialog = modalEl.querySelector('.modal-dialog'); 143 if (!dialog) return; 144 145 // admin-modal khi á» login state â thu nhá» như small modal 146 if (modalId === 'admin-modal' && isAdminLoginState()) { 147 fixSmallModalPosition(modalEl); 148 return; 149 } 150 151 var bottomMargin = window.innerHeight < 800 ? 2 : 4; 152 var availH = window.innerHeight - navH - gap - bottomMargin; 153 dialog.style.marginTop = (navH + gap) + 'px'; 154 dialog.style.marginBottom = bottomMargin + 'px'; 155 dialog.style.height = availH + 'px'; 156 dialog.style.maxHeight = availH + 'px'; 157 dialog.style.minHeight = availH + 'px'; 158 } 159 160 // ââ Fix small modal: chá» Äẩy xuá»ng dưá»i navbar, KHÃNG set height âââââââââ 161 function fixSmallModalPosition(modalEl) { 162 // Small modals: SCALING_JS (applySmallModalStyle) Äã xá» lý Äầy Äá»§ 163 // transform:scale, margin-top, width => modal_layout.js chá» cần 164 // reset paddingTop cá»§a wrapper Äá» không bá» Bootstrap padding Äẩy thêm. 165 modalEl.style.paddingTop = '0px'; 166 modalEl.style.paddingBottom = '0px'; 167 168 var dialog = modalEl.querySelector('.modal-dialog'); 169 if (!dialog) return; 170 171 // Xóa inline height nếu Äã từng bá» set từ trưá»c (và dụ khi switch modal type) 172 dialog.style.height = ''; 173 dialog.style.minHeight = ''; 174 // KHÃNG set marginTop hay width á» Äây â applySmallMod
174alStyle lo viá»c Äó. 175 } 176 177 // ââ Fix auto-height modal: shrink-to-fit content, max = viewport - navbar â 178 function fixAutoHeightModalPosition(modalEl) { 179 var navH = getNavbarHeight(); 180 var gap = window.innerHeight < 800 ? 4 : 8; 181 var bottomGap = window.innerHeight < 800 ? 4 : 8; 182 var maxH = window.innerHeight - navH - gap - bottomGap; 183 184 modalEl.style.paddingTop = '0px'; 185 modalEl.style.paddingBottom = '0px'; 186 187 var dialog = modalEl.querySelector('.modal-dialog'); 188 if (!dialog) return; 189 190 // marginTop clears navbar; no fixed height â shrinks to content 191 dialog.style.marginTop = (navH + gap) + 'px'; 192 dialog.style.marginBottom = bottomGap + 'px'; 193 dialog.style.maxHeight = maxH + 'px'; 194 // Remove any fixed height previously set (e.g. if modal was in LARGE list before) 195 dialog.style.height = ''; 196 dialog.style.minHeight = ''; 197 198 // Ensure modal-content also respects maxHeight 199 var content = dialog.querySelector('.modal-content'); 200 if (content) { 201 content.style.maxHeight = maxH + 'px'; 202 } 203 } 204 205 // ââ Fix about-modal column layout (issues 5, 6) ââââââââââââââââââââââââââ 206 function fixAboutModalCols() { 207 var modalBody = document.querySelector('#about-modal .modal-body'); 208 var row = document.querySelector('#about-modal .modal-body > .row'); 209 var cols = document.querySelectorAll( 210 '#about-modal .modal-body > .row > [class*="col-"]' 211 ); 212 if (!modalBody || !row || !cols.length) return; 213 214 var bodyH = Math.round(modalBody.getBoundingClientRect().height); 215 if (bodyH < 50) return; 216 217 row.style.height = bodyH + 'px'; 218 row.style.maxHeight = bodyH + 'px'; 219 row.style.minHeight = bodyH + 'px'; 220 row.style.overflow = 'hidden'; 221 row.style.display = 'flex'; 222 row.style.flexWrap = 'nowrap'; 223 row.style.alignItems = 'stretch'; 224 225 cols.forEach(function (col) { 226 col.style.height = bodyH + 'px'; 227 col.style.maxHeight = bodyH + 'px'; 228 col.style.minHeight = bodyH + 'px'; 229 col.style.overflow = 'hidden'; 230 col.style.display = 'flex'; 231 col.style.flexDirection = 'column'; 232 233 var inner = col.firstElementChild; 234 if (inner) { 235 inner.style.flex = '1 1 auto'; 236 inner.style.minHeight = '0'; 237 inner.style.overflowY = 'auto'; 238 inner.style.overflowX = 'hidden'; 239 } 240 }); 241 242 var col1 = cols[0]; 243 if (col1) { 244 var col1inner = col1.firstElementChild; 245 if (col1inner) { 246 col1inner.style.display = 'flex'; 247 col1inner.style.flexDirection = 'column'; 248 var footer = col1inner.lastElementChild; 249 if (footer) footer.style.marginTop = 'auto'; 250 } 251 } 252 253 var col3 = cols[cols.length - 1]; 254 if (col3) { 255 var col3inner = col3.firstElementChild; 256 if (col3inner) { 257 col3inner.style.display = 'flex'; 258 col3inner.style.flexDirection = 'column'; 259 col3inner.style.overflow = 'hidden'; 260 261 var readmeCard = col3inner.firstElementChild; 262 if (readmeCard) { 263 readmeCard.style.flex = '1 1 auto'; 264 readmeCard.style.minHeight = '0'; 265 readmeCard.style.display = 'flex'; 266 readmeCard.style.flexDirection = 'column'; 267 readmeCard.style.overflow = 'hidden'; 268 } 269 } 270 271 var readme = document.getElementById('about-readme-content'); 272 if (readme) { 273 readme.style.flex = '1 1 auto'; 274 readme.style.minHeight = '0'; 275 readme.style.maxHeight = '100%'; 276 readme.style.overflowY = 'auto'; 277 readme.style.overflowX = 'hidden'; 278 } 279 } 280 } 281 282 // ââ Fix #9: Click outside modal to close âââââââââââââââââââââââââââââââââ 283 function setupBackdropClose(modalId, closeBtnId) { 284 // Don't setup backdrop close for null entries (keep static)
285 if (closeBtnId === null && modalId !== 'about-modal') return; 286 287 var modalEl = document.getElementById(modalId); 288 if (!modalEl) return; 289 290 // Prevent double-binding 291 if (modalEl.__backdropCloseSetup) return; 292 modalEl.__backdropCloseSetup = true; 293 294 modalEl.addEventListener('mousedown', function(e) { 295 // Check if click was directly on the modal backdrop (the outermost .modal element) 296 if (e.target !== modalEl) return; 297 // Modal must be open 298 if (!modalEl.classList.contains('show')) return; 299 300 if (closeBtnId) { 301 // Click the designated close button â this triggers Dash callback 302 // which checks unsaved changes before closing 303 var closeBtn = document.getElementById(closeBtnId); 304 if (closeBtn) { 305 closeBtn.click(); 306 } 307 } else { 308 // Find built-in Bootstrap close button (for modals with close_button=True) 309 var builtInClose = modalEl.querySelector('.modal-header .btn-close, .modal-header [aria-label="Close"]'); 310 if (builtInClose) { 311 builtInClose.click(); 312 } 313 } 314 }); 315 } 316 317 // ââ Observe modal khi má»/Äóng ââââââââââââââââââââââââââââââââââââââââââââ 318 function observeModal(modalId, isLarge, isAutoHeight) { 319 var modalEl = document.getElementById(modalId); 320 if (!modalEl) return; 321 322 var observer = new MutationObserver(function () { 323 var isOpen = modalEl.classList.contains('show'); 324 if (!isOpen) return; 325 326 if (isLarge) { 327 fixLargeModalPosition(modalEl, modalId); 328 if (modalId === 'about-modal') { 329 setTimeout(fixAboutModalCols, 50); 330 setTimeout(fixAboutModalCols, 300); 331 } 332 } else if (isAutoHeight) { 333 fixAutoHeightModalPosition(modalEl); 334 } else { 335 fixSmallModalPosition(modalEl); 336 } 337 }); 338 339 observer.observe(modalEl, { 340 attributes: true, 341 attributeFilter: ['class', 'style'], 342 }); 343 344 // admin-modal: theo dõi thêm login/settings section Äá» phát hiá»n state change 345 if (modalId === 'admin-modal') { 346 var watchSection = function(sectionId) { 347 var section = document.getElementById(sectionId); 348 if (!section) return; 349 new MutationObserver(function () { 350 updateAdminModalState(); 351 if (modalEl.classList.contains('show')) { 352 fixLargeModalPosition(modalEl, 'admin-modal'); 353 } 354 }).observe(section, { attributes: true, attributeFilter: ['style'] }); 355 }; 356 // Delay vì Dash render section sau khi modal mount 357 setTimeout(function() { 358 watchSection('admin-login-section'); 359 watchSection('admin-settings-section'); 360 }, 1000); 361 } 362 } 363 364 // ââ ResizeObserver: re-sync khi window resize hoặc zoom thay Äá»i âââââââââ 365 function watchResize() { 366 if (typeof ResizeObserver === 'undefined') return; 367 368 var ro = new ResizeObserver(function () { 369 LARGE_MODAL_IDS.forEach(function (id) { 370 var el = document.getElementById(id); 371 if (el && el.classList.contains('show')) { 372 fixLargeModalPosition(el, id); 373 if (id === 'about-modal') fixAboutModalCols(); 374 } 375 }); 376 AUTO_HEIGHT_MODAL_IDS.forEach(function (id) { 377 var el = document.getElementById(id); 378 if (el && el.classList.contains('show')) { 379 fixAutoHeightModalPosition(el); 380 } 381 }); 382 SMALL_MODAL_IDS.forEach(function (id) { 383 var el = document.getElementById(id); 384 if (el && el.classList.contains('show')) {
385 fixSmallModalPosition(el); 386 } 387 }); 388 }); 389 390 ro.observe(document.documentElement); 391 } 392 393 // ââ Init âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 394 function init() { 395 LARGE_MODAL_IDS.forEach(function (id) { observeModal(id, true, false); }); 396 AUTO_HEIGHT_MODAL_IDS.forEach(function (id) { observeModal(id, false, true); }); 397 SMALL_MODAL_IDS.forEach(function (id) { observeModal(id, false, false); }); 398 watchResize(); 399 400 // Fix #9: Setup backdrop click-to-close for configured modals 401 // Use setTimeout to ensure DOM is ready 402 setTimeout(function() { 403 Object.keys(BACKDROP_CLOSE_CONFIG).forEach(function(modalId) { 404 var closeBtnId = BACKDROP_CLOSE_CONFIG[modalId]; 405 setupBackdropClose(modalId, closeBtnId); 406 }); 407 }, 1500); 408 } 409 410 if (document.readyState === 'loading') { 411 document.addEventListener('DOMContentLoaded', init); 412 } else { 413 var retries = 0; 414 var interval = setInterval(function () { 415 var found = ALL_MODAL_IDS.some(function (id) { 416 return document.getElementById(id); 417 }); 418 if (found || retries++ > 20) { 419 clearInterval(interval); 420 init(); 421 } 422 }, 500); 423 } 424 425})(); 426/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 427 FIX #4: Portal-style fix for dcc.Dropdown inside overflow:hidden 428 Uses MutationObserver to detect when role dropdown opens and 429 repositions the menu absolutely relative to viewport. 430 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 431(function() { 432 function fixDropdownMenu(menuOuter) { 433 if (!menuOuter || menuOuter._asm_fixed) return; 434 const control = menuOuter.closest('.Select'); 435 if (!control) return; 436 const rect = control.getBoundingClientRect(); 437 // FIX (scale bug â mà n hình nhá» vd laptop 14"): applyScale() (xem 438 // utils_js.py) Äặt transform:scale(X) lên #react-entry-point khi 439 // window.outerWidth < 1920. Theo spec CSS, Má»I ancestor có transform 440 // sẽ trá» thà nh "containing block" cho các phần tá» con position: 441 // fixed â nghÄ©a là "top/left" bên dưá»i không còn tÃnh theo viewport 442 // THẬT nữa, mà tÃnh theo há» toạ Äá» LOCAL (chưa scale) cá»§a 443 // #react-entry-point. rect.* từ getBoundingClientRect() lại LUÃN trả 444 // vá» toạ Äá» THẬT (Äã scale) â nên nếu gán thẳng rect.bottom/rect.left 445 // là m top/left px cho 1 phần tá» VẪN NẰM Lá»NG bên trong 446 // #react-entry-point (như menuOuter á» Äây â nó không ÄÆ°á»£c portal ra 447 // <body>), kết quả sẽ bá» lá»ch Äúng bằng há» sá» scale (menu bá» "co" 448 // lại vá» phÃa góc trên-trái). Chia ngược lại cho scale Äá» quy Äá»i vá» 449 // Äúng há» toạ Äá» LOCAL trưá»c khi gán. 450 const scale = window._appScale || 1; 451 const rectBottom = rect.bottom / scale; 452 const rectLeft = rect.left / scale; 453 const rectWidth = rect.width / scale; 454 // Calculate available space below 455 const spaceBelow = window.innerHeight - rect.bottom - 10; 456 const menuH = Math.min(200, Math.max(100, spaceBelow)); 457 // Use fixed positioning to escape overflow:hidden/auto ancestors 458 menuOuter.style.position = 'fixed'; 459 menuOuter.style.top = rectBottom + 'px'; 460 menuOuter.style.left = rectLeft + 'px'; 461 menuOuter.style.width = Math.max(rectWidth, 130) + 'px'; 462 menuOuter.style.minWidth = Math.max(rectWidth, 130) + 'px'; 463 menuOuter.style.maxHeight = menuH + 'px'; 464 menuOuter.style.zIndex = '999999'; 465 menuOuter.style.overflow = 'auto'; 466 menuOuter.style.boxShadow = '0 4px 20px rgba(0,0,0,0.35)'; 467 menuOuter._asm_fixed = true; 468 // Re-position if control moves (e.g. on scroll) 469 const scrollParent = menuOuter.closest('#user-list-container, .modal-body'); 470 if (scrollParent) { 471 const onScroll = function() { 472 const r2 = control.getBoundingClientRect(); 473 const s2 = window._appScale || 1; 474 menuOuter.style.top = (r2.bottom / s2) + 'px'; 475 menuOuter.style.left = (r2.left / s2) + 'px'; 476 }; 477 scrollParent.addEventListener('scroll', onScroll, { passive: true }); 478 menuOuter._asm_scroll_cleanup = function() { 479 scrollParent.removeEventListener('scroll', onScroll); 480 }; 481 } 482 } 483 484 function observeDropdowns() { 485 const adminModal = document.getElementById('admin-modal'); 486 if (!adminModal) return; 487 const obs = new MutationObserver(function(mutations) { 488 mutations.forEach(function(m) { 489 // Clean up removed menus
490 m.removedNodes.forEach(function(node) { 491 if (node._asm_scroll_cleanup) node._asm_scroll_cleanup(); 492 }); 493 m.addedNodes.forEach(function(node) { 494 if (node.nodeType !== 1) return; 495 // Check if it's a dropdown menu inside new-user-role or user list role dropdowns 496 if (node.classList && (node.classList.contains('Select-menu-outer') || node.classList.contains('Select-menu'))) { 497 const parent = node.closest('#new-user-role, .new-user-role-dropdown, [id^="user-role-"], .user-role-dropdown'); 498 if (parent) fixDropdownMenu(node); 499 } 500 // Also check children 501 node.querySelectorAll && node.querySelectorAll('.Select-menu-outer').forEach(function(menu) { 502 const parent = menu.closest('#new-user-role, .new-user-role-dropdown, [id^="user-role-"], .user-role-dropdown'); 503 if (parent) fixDropdownMenu(menu); 504 }); 505 }); 506 }); 507 }); 508 obs.observe(adminModal, { childList: true, subtree: true }); 509 } 510 511 // Start observing when DOM is ready 512 if (document.readyState === 'loading') { 513 document.addEventListener('DOMContentLoaded', observeDropdowns); 514 } else { 515 observeDropdowns(); 516 } 517 // PERF FIX: Replaced 2000ms setInterval with MutationObserver. 518 // Watch for admin-modal being added to DOM instead of polling. 519 if (!window._asm_dropdown_obs_started) { 520 var _adminModalWatcher = new MutationObserver(function() { 521 if (!window._asm_dropdown_obs_started) { 522 var modal = document.getElementById('admin-modal'); 523 if (modal) { 524 observeDropdowns(); 525 window._asm_dropdown_obs_started = true; 526 _adminModalWatcher.disconnect(); 527 } 528 } 529 }); 530 _adminModalWatcher.observe(document.body, { childList: true, subtree: true }); 531 } 532})(); 533 534/* ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 535 FIX #2: Auto-clear session-details-msg after 4s 536 MutationObserver detects when message changes, resets CSS animation 537 so it works every time (not just first time). 538 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 539(function() { 540 var _msgTimer = null;
541 function watchSessionMsg() { 542 var msgEl = document.getElementById('session-details-msg'); 543 if (!msgEl) return; 544 var obs = new MutationObserver(function() { 545 if (!msgEl.textContent.trim() && !msgEl.innerHTML.trim()) return; 546 // Reset animation by cloning 547 msgEl.style.animation = 'none'; 548 msgEl.offsetHeight; // reflow 549 msgEl.style.animation = ''; 550 // Also hard-clear after 4s to ensure it's gone 551 clearTimeout(_msgTimer); 552 _msgTimer = setTimeout(function() { 553 msgEl.style.opacity = '0'; 554 setTimeout(function() { 555 // Actually clear via Dash would require a callback. 556 // Just hide it visually â Dash will set to "" on next force logout click 557 msgEl.innerHTML = ''; 558 msgEl.style.opacity = ''; 559 }, 300); 560 }, 4000); 561 }); 562 obs.observe(msgEl, { childList: true, subtree: true, characterData: true }); 563 } 564 // Start watching when DOM ready 565 if (document.readyState === 'loading') { 566 document.addEventListener('DOMContentLoaded', watchSessionMsg); 567 } else {
568 watchSessionMsg(); 569 } 570 // PERF FIX: Replaced 2000ms setInterval with MutationObserver. 571 // Watch for session-details-msg being added to DOM instead of polling. 572 var _sessionMsgWatcher = new MutationObserver(function() { 573 var msgEl = document.getElementById('session-details-msg'); 574 if (msgEl && !msgEl._asm_msg_watched) { 575 msgEl._asm_msg_watched = true; 576 watchSessionMsg(); 577 } 578 }); 579 _sessionMsgWatcher.observe(document.body, { childList: true, subtree: true }); 580})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.