PageSourceSearch

https://hallocentrumeiland.nl/web/js/leaflet.popup-portal/leaflet.popup-portal.js?modified=1773741795000

js hallocentrumeiland.nl collected 2026-10-02 23:40:56 UTC 14,883 bytes, 369 lines download raw bytes

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.