1/** 2 * Leaflet.PopupPortal 3 * 4 * Problem: .leaflet-map-pane uses transform: translate3d() which creates a new 5 * CSS stacking context. Any popup inside that pane cannot escape its z-index 6 * ceiling (~400), so Leaflet controls (outside the pane) always render on top. 7 * 8 * Solution: When a popup opens, teleport its container element to a fixed 9 * overlay attached to <body> (or active fullscreen root). Position is 10 * recalculated from the 11 * popup's latlng on every event that could move or resize the popup anchor 12 * (map move, zoom, scroll, resize, viewreset). The close button is rewired 13 * directly so it works outside the original popup pane. All Leaflet events 14 * and programmatic open/close continue to work normally. 15 * 16 * Usage: include this file after leaflet.js. No configuration needed. 17 * Map option: { popupPortal: false } to opt a specific map instance out. 18 */ 19(function (L) { 20 'use strict'; 21 22 if (typeof L === 'undefined' || !L.Popup) { 23 return; 24 } 25 26 var PORTAL_CLOSE_THRESHOLD = 100; 27 var PORTAL_FADE_DURATION = 200; 28 var PORTAL_CLASS_WHITELIST = { 29 'leaflet-touch': true, 30 'leaflet-retina': true, 31 'leaflet-oldie': true, 32 'leaflet-safari': true, 33 'leaflet-fade-anim': true, 34 'leaflet-zoom-anim': true 35 }; 36 37 /* âââ Portal overlay âââââââââââââââââââââââââââââââââââââââââââ */ 38 39 var _portal = null; 40 41 function getDocumentFullscreenElement() { 42 return document.fullscreenElement || 43 document.mozFullScreenElement || 44 document.webkitFullscreenElement || 45 document.msFullscreenElement || 46 null; 47 } 48 49 function getPortalHostForMap(map) { 50 var fullscreenElement = getDocumentFullscreenElement(); 51 var mapContainer = map && map._container; 52 53 if (fullscreenElement && mapContainer && fullscreenElement.contains(mapContainer)) { 54 return fullscreenElement; 55 } 56 57 return document.body; 58 } 59 60 function ensurePortalHost(map) { 61 var portal = getPortal(); 62 var host = getPortalHostForMap(map); 63 64 if (portal.parentNode !== host) { 65 host.appendChild(portal); 66 } 67 68 return portal; 69 } 70 71 function getPortal() { 72 if (!_portal) { 73 _portal = document.createElement('div'); 74 _portal.id = 'leaflet-popup-portal'; 75 _portal.className = 'leaflet-popup-portal leaflet-container'; 76 _portal.style.cssText = [ 77 'position:fixed', 78 'top:0', 79 'left:0', 80 'width:0', 81 'height:0', 82 'z-index:10000', 83 'pointer-events:none', 84 'overflow:visible', 85 'background:transparent' 86 ].join(';'); 87 document.body.appendChild(_portal); 88 } 89 return _portal; 90 } 91 92 function syncPortalLeafletClasses(map) { 93 var portal = ensurePortalHost(map); 94 var container = map && map._container; 95 var classes = ['leaflet-popup-portal', 'leaflet-container']; 96 97 if (!container || !container.classList) { 98 portal.className = classes.join(' '); 99 return portal; 100 } 101 102 Array.prototype.forEach.call(container.classList, function (className) { 103 if (PORTAL_CLASS_WHITELIST[className]) { 104 classes.push(className); 105 } 106 }); 107 108 portal.className = classes.join(' '); 109 return portal; 110 } 111 112 /* âââ Override _updatePosition âââââââââââââââââââââââââââââââââ */ 113 // Leaflet calls _updatePosition on every map move/zoom. 114 // When the popup is in the portal we redirect to our own sync logic. 115 116 var _origUpdatePosition = L.Popup.prototype._updatePosition; 117 var _origAnimateZoom = L.Popup.prototype._animateZoom; 118 119 L.Popup.prototype._updatePosition = function () { 120 if (this._inPortal) { 121 this._syncPortalPosition(); 122 return; 123 } 124 _origUpdatePosition.call(this); 125 }; 126 127 L.Popup.prototype._animateZoom = function (e) { 128 if (this._inPortal) { 129 this._syncPortalPosition(e); 130 return; 131 } 132 _origAnimateZoom.call(this, e); 133 }; 134 135 L.Popup.prototype._setPortalVisibility = function (isVisible) { 136 var container = this._container; 137 138 if (!container || this._portalVisible === isVisible) { 139 return; 140 } 141 142 this._portalVisible = isVisible; 143 container.style.opacity = isVisible ? '1' : '0'; 144 container.style.pointerEvents = isVisible ? 'auto' : 'none'; 145 }; 146 147 /* âââ Position sync ââââââââââââââââââââââââââââââââââââââââââââ */ 148 149 /** 150 * Recalculates and applies position:fixed top/left for the portal popup. 151 * 152 * Strategy: replicate Leaflet's own _updatePosition math but in fixed 153 * viewport coordinates instead of pane-relative coordinates. 154 * 155 * Leaflet places the popup so that the tip of the arrow sits exactly at
156 * the anchor point of the source layer (marker). It computes: 157 * pos = map.latLngToLayerPoint(latlng) â in layer (pane) pixels 158 * pos += anchor â layer's icon anchor 159 * pos += popup._getAnchor() â popup tip offset 160 * then positions the container bottom at pos.y, left-centre at pos.x 161 * 162 * We translate the same pos to fixed-viewport coords via mapRect, skipping 163 * the pane transform entirely. Container size is read fresh on every call 164 * so zoom-induced size changes are handled automatically. 165 */ 166 L.Popup.prototype._syncPortalPosition = function (zoomEvent) { 167 var container = this._container; 168 var map = this._map; 169 if (!container || !map || !this._latlng) { return; } 170 171 // Guard against infinite recursion: _update() calls _updatePosition() 172 // which calls _syncPortalPosition() again. With the guard active the 173 // nested call returns immediately; _update() still performs its content 174 // and layout passes so container dimensions are correct when we read 175 // them below. 176 if (!zoomEvent) { 177 if (this._syncingPortalPos) { return; } 178 this._syncingPortalPos = true; 179 180 try { 181 // Re-run Leaflet's own layout pass so _containerWidth/Height and the 182 // popup tip anchor are up to date (important after zoom). 183 if (this._update) { this._update(); } 184 } finally { 185 this._syncingPortalPos = false; 186 } 187 } 188 189 var mapRect = map._container.getBoundingClientRect(); 190 191 // During zoom animation Leaflet positions layers against the incoming 192 // animated center/zoom, not the current settled view. 193 var pos = zoomEvent 194 ? map._latLngToNewLayerPoint(this._latlng, zoomEvent.zoom, zoomEvent.center) 195 .add(map._getMapPanePos()) 196 : map.latLngToContainerPoint(this._latlng); 197 198 // _getAnchor() already incorporates _source._getPopupAnchor() plus any 199 // options.offset. Do NOT add the source anchor separately â that would 200 // double-count it (causing ~30 px vertical offset). 201 var anchor = this._getAnchor ? this._getAnchor() : L.point(0, 0); 202 pos = pos.add(anchor); 203 204 // Container dimensions (updated by _update() above). 205 var cw = container.offsetWidth; 206 var ch = container.offsetHeight; 207 208 // Centre horizontally and align container bottom to the anchor point. 209 var fixedLeft = mapRect.left + pos.x - Math.round(cw / 2); 210 var fixedTop = mapRect.top + pos.y - ch; 211 212 container.style.left = fixedLeft + 'px'; 213 container.style.top = fixedTop + 'px'; 214 container.style.transform = 'none'; 215 216 this._setPortalVisibility(!( 217 fixedLeft < mapRect.left - PORTAL_CLOSE_THRESHOLD || 218 fixedLeft + cw > mapRect.right + PORTAL_CLOSE_THRESHOLD || 219 fixedTop < mapRect.top - PORTAL_CLOSE_THRESHOLD || 220 fixedTop + ch > mapRect.bottom + PORTAL_CLOSE_THRESHOLD 221 )); 222 }; 223 224 /* âââ Map hook âââââââââââââââââââââââââââââââââââââââââââââââââ */ 225 226 L.Map.addInitHook(function () { 227 var map = this; 228 229 /* ââ popup open: teleport to portal ââ */ 230 map.on('popupopen', function (e) { 231 var popup = e.popup; 232 var container = popup._container; 233 if (!container || popup._inPortal) { return; } 234 if (map.options.popupPortal === false) { return; } 235 236 if (popup._portalCloseTimer) { 237 clearTimeout(popup._portalCloseTimer); 238 popup._portalCloseTimer = null; 239 } 240 241 // Move element out of the leaflet-map-pane stacking context. 242 syncPortalLeafletClasses(map).appendChild(container); 243 popup._inPortal = true; 244 245 // Switch from Leaflet's pane-relative bottom/left scheme to 246 // position:fixed top/left so we can place it in viewport space. 247 container.style.position = 'fixed'; 248 container.style.transform = 'none'; 249 container.style.bottom = 'auto'; 250 container.style.marginBottom = '0'; 251 container.style.pointerEvents = 'auto'; 252 container.style.opacity = '1'; 253 container.style.transition = 'opacity ' + PORTAL_FADE_DURATION + 'ms ease'; 254 container.style.willChange = 'left, top'; 255 popup._portalVisible = true; 256 257 // Calculate and apply the correct fixed position immediately. 258 popup._syncPortalPosition(); 259 260 // ââ Close button ââââââââââââââââââââââââââââââââââââââââââââââ 261 // Leaflet attaches the click handler for the close button on the 262 // popup pane element via event delegation. Once we move the 263 // container out of that pane the delegation no longer fires. 264 // Rewire the button directly on the container itself. 265 var closeBtn = container.querySelector('.leaflet-popup-close-button'); 266 if (closeBtn && !closeBtn._portalWired) { 267 closeBtn._portalWired = true; 268 L.DomEvent.on(closeBtn, 'click', function (ev) { 269 L.DomEvent.preventDefault(ev); 270 popup._close(); 271 }); 272 } 273 }); 274 275 /* ââ popup close: fade in portal, cleanup after fade ââ */ 276 map.on('popupclose', function (e) { 277 var popup = e.popup; 278 var container = popup._container; 279 if (!popup._inPortal || !container) { return; } 280 281 // Keep current fixed coordinates for the whole fade-out so there is 282 // no visible jump between viewport-fixed and pane-relative spaces. 283 container.style.transition = 'opacity ' + PORTAL_FADE_DURATION + 'ms ease'; 284 container.style.pointerEvents = 'none'; 285 container.style.opacity = '0'; 286 287 popup._inPortal = false;
288 popup._portalVisible = false; 289 290 if (popup._portalCloseTimer) { 291 clearTimeout(popup._portalCloseTimer); 292 } 293 294 popup._portalCloseTimer = setTimeout(function () { 295 popup._portalCloseTimer = null; 296 297 // If popup was reopened during the timeout, do not reset styles. 298 if (popup._inPortal) { return; } 299 300 var pane = map.getPane('popupPane'); 301 if (pane && container.parentNode) { 302 pane.appendChild(container); 303 } 304 305 // Restore Leaflet's default inline styles so the next open is clean. 306 container.style.position = ''; 307 container.style.left = ''; 308 container.style.top = ''; 309 container.style.transform = ''; 310 container.style.bottom = ''; 311 container.style.marginBottom = ''; 312 container.style.pointerEvents = ''; 313 container.style.opacity = ''; 314 container.style.transition = ''; 315 container.style.willChange = ''; 316 popup._portalVisible = true; 317 }, PORTAL_FADE_DURATION); 318 }); 319 320 /* ââ keep portal popup in sync on anything that moves the anchor ââ */ 321 // 322 // Events covered: 323 // move / moveend â map pan 324 // zoom / zoomend â zoom level change (also changes popup size) 325 // viewreset â full redraw (e.g. CRS change) 326 // resize â browser window / map container resize 327 // scroll â page scroll (changes mapRect from getBoundingClientRect) 328 // 329 map.on('move zoom moveend zoomend viewreset resize', function () { 330 var popup = map._popup; 331 if (popup && popup._inPortal) { 332 ensurePortalHost(map); 333 popup._syncPortalPosition(); 334 } 335 }); 336 337 // In native fullscreen only descendants of the fullscreen root are visible. 338 // Rehost portal there (and back to body on exit) to keep popups visible. 339 map.on('fullscreenchange', function () { 340 ensurePortalHost(map); 341 342 var popup = map._popup; 343 if (popup && popup._inPortal) { 344 popup._syncPortalPosition(); 345 } 346 }); 347 348 // Window scroll shifts mapRect in the viewport â sync on that too. 349 function onWindowScroll() { 350 var popup = map._popup; 351 if (popup && popup._inPortal) { 352 ensurePortalHost(map); 353 popup._syncPortalPosition(); 354 } 355 } 356 window.addEventListener('scroll', onWindowScroll, { passive: true, capture: true }); 357 358 // Clean up the scroll listener when the map is removed. 359 map.on('unload', function () { 360 var popup = map._popup; 361 if (popup && popup._portalCloseTimer) { 362 clearTimeout(popup._portalCloseTimer); 363 popup._portalCloseTimer = null; 364 } 365 window.removeEventListener('scroll', onWindowScroll, { capture: true }); 366 }); 367 }); 368 369}(typeof L !== 'undefined' ? L : null));
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.