mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-03-28 17:42:48 +01:00
Ensure node overlays appear above fullscreen map (#333)
* Increase overlay z-index to surface node info * Ensure short info overlays attach to fullscreen host * Ensure info overlay participates in fullscreen mode
This commit is contained in:
@@ -238,6 +238,7 @@ class StubElement {
|
||||
function createStubDom() {
|
||||
const body = new StubElement('body');
|
||||
body.contains = body.contains.bind(body);
|
||||
const listenerMap = new Map();
|
||||
const document = {
|
||||
body,
|
||||
documentElement: { clientWidth: 640, clientHeight: 480 },
|
||||
@@ -247,6 +248,26 @@ function createStubDom() {
|
||||
getElementById() {
|
||||
return null;
|
||||
},
|
||||
addEventListener(event, handler) {
|
||||
if (!listenerMap.has(event)) {
|
||||
listenerMap.set(event, new Set());
|
||||
}
|
||||
listenerMap.get(event).add(handler);
|
||||
},
|
||||
removeEventListener(event, handler) {
|
||||
if (!listenerMap.has(event)) {
|
||||
return;
|
||||
}
|
||||
listenerMap.get(event).delete(handler);
|
||||
},
|
||||
_dispatch(event) {
|
||||
if (!listenerMap.has(event)) {
|
||||
return;
|
||||
}
|
||||
for (const handler of Array.from(listenerMap.get(event))) {
|
||||
handler();
|
||||
}
|
||||
},
|
||||
};
|
||||
const window = {
|
||||
scrollX: 10,
|
||||
@@ -282,6 +303,7 @@ test('render opens overlays and positions them relative to anchors', () => {
|
||||
assert.equal(open.length, 1);
|
||||
const overlay = open[0].element;
|
||||
assert.equal(overlay.parentNode, body);
|
||||
assert.equal(overlay.style.position, 'absolute');
|
||||
const content = overlay.querySelector('.short-info-content');
|
||||
assert.ok(content);
|
||||
assert.equal(content.innerHTML, '<strong>Node</strong>');
|
||||
@@ -340,6 +362,31 @@ test('containsNode recognises overlay descendants', () => {
|
||||
assert.equal(stack.containsNode(stray), false);
|
||||
});
|
||||
|
||||
test('overlays migrate into and out of fullscreen hosts', () => {
|
||||
const { document, window, factory, anchor, body } = createStubDom();
|
||||
const fullscreenRoot = document.createElement('div');
|
||||
body.appendChild(fullscreenRoot);
|
||||
const stack = createShortInfoOverlayStack({ document, window, factory });
|
||||
stack.render(anchor, 'Fullscreen');
|
||||
const [entry] = stack.getOpenOverlays();
|
||||
assert.equal(entry.element.parentNode, body);
|
||||
assert.equal(entry.element.style.position, 'absolute');
|
||||
|
||||
document.fullscreenElement = fullscreenRoot;
|
||||
document._dispatch('fullscreenchange');
|
||||
assert.equal(entry.element.parentNode, fullscreenRoot);
|
||||
assert.equal(entry.element.style.position, 'fixed');
|
||||
assert.equal(entry.element.style.left, '40px');
|
||||
assert.equal(entry.element.style.top, '50px');
|
||||
|
||||
document.fullscreenElement = null;
|
||||
document._dispatch('fullscreenchange');
|
||||
assert.equal(entry.element.parentNode, body);
|
||||
assert.equal(entry.element.style.position, 'absolute');
|
||||
assert.equal(entry.element.style.left, '50px');
|
||||
assert.equal(entry.element.style.top, '70px');
|
||||
});
|
||||
|
||||
test('rendered overlays do not swallow click events by default', () => {
|
||||
const { document, window, factory, anchor } = createStubDom();
|
||||
const stack = createShortInfoOverlayStack({ document, window, factory });
|
||||
|
||||
@@ -58,6 +58,9 @@ export function initializeApp(config) {
|
||||
const baseTitle = document.title;
|
||||
const nodesTable = document.getElementById('nodes');
|
||||
const sortButtons = nodesTable ? Array.from(nodesTable.querySelectorAll('thead .sort-button[data-sort-key]')) : [];
|
||||
const infoOverlayHome = infoOverlay
|
||||
? { parent: infoOverlay.parentNode, nextSibling: infoOverlay.nextSibling }
|
||||
: null;
|
||||
/**
|
||||
* Column sorter configuration for the node table.
|
||||
*
|
||||
@@ -498,6 +501,62 @@ export function initializeApp(config) {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Append the informational modal overlay to the fullscreen container when active.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function attachInfoOverlayToFullscreenHost() {
|
||||
if (!infoOverlay || !fullscreenContainer) return;
|
||||
if (infoOverlay.parentNode !== fullscreenContainer) {
|
||||
fullscreenContainer.appendChild(infoOverlay);
|
||||
}
|
||||
if (infoOverlay.classList) {
|
||||
infoOverlay.classList.add('info-overlay--fullscreen');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the informational overlay to its original DOM position.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function restoreInfoOverlayToHome() {
|
||||
if (!infoOverlay || !infoOverlayHome || !infoOverlayHome.parent) return;
|
||||
if (infoOverlay.parentNode === infoOverlayHome.parent) {
|
||||
if (infoOverlay.classList) {
|
||||
infoOverlay.classList.remove('info-overlay--fullscreen');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
infoOverlayHome.nextSibling &&
|
||||
infoOverlayHome.nextSibling.parentNode === infoOverlayHome.parent &&
|
||||
typeof infoOverlayHome.parent.insertBefore === 'function'
|
||||
) {
|
||||
infoOverlayHome.parent.insertBefore(infoOverlay, infoOverlayHome.nextSibling);
|
||||
} else if (typeof infoOverlayHome.parent.appendChild === 'function') {
|
||||
infoOverlayHome.parent.appendChild(infoOverlay);
|
||||
}
|
||||
if (infoOverlay.classList) {
|
||||
infoOverlay.classList.remove('info-overlay--fullscreen');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the informational overlay participates in the active fullscreen subtree.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function syncInfoOverlayHost() {
|
||||
if (!infoOverlay) return;
|
||||
if (isMapInFullscreen()) {
|
||||
attachInfoOverlayToFullscreenHost();
|
||||
} else {
|
||||
restoreInfoOverlayToHome();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Respond to fullscreen change events originating from the browser.
|
||||
*
|
||||
@@ -528,6 +587,7 @@ export function initializeApp(config) {
|
||||
mapContainer.style.minHeight = '';
|
||||
}
|
||||
}
|
||||
syncInfoOverlayHost();
|
||||
updateFullscreenToggleState();
|
||||
refreshMapSize();
|
||||
}
|
||||
@@ -548,6 +608,8 @@ export function initializeApp(config) {
|
||||
}
|
||||
}
|
||||
|
||||
syncInfoOverlayHost();
|
||||
|
||||
// Firmware 2.7.10 / Android 2.7.0 roles and colors (see issue #177)
|
||||
const roleColors = Object.freeze({
|
||||
CLIENT_HIDDEN: '#A9CBE8',
|
||||
@@ -1355,6 +1417,7 @@ export function initializeApp(config) {
|
||||
*/
|
||||
function openInfoOverlay() {
|
||||
if (!infoOverlay || !infoDialog) return;
|
||||
syncInfoOverlayHost();
|
||||
lastFocusBeforeInfo = document.activeElement;
|
||||
infoOverlay.hidden = false;
|
||||
document.body.style.setProperty('overflow', 'hidden');
|
||||
|
||||
@@ -15,6 +15,61 @@
|
||||
*/
|
||||
|
||||
const DEFAULT_TEMPLATE_ID = 'shortInfoOverlayTemplate';
|
||||
const FULLSCREEN_CHANGE_EVENTS = [
|
||||
'fullscreenchange',
|
||||
'webkitfullscreenchange',
|
||||
'mozfullscreenchange',
|
||||
'MSFullscreenChange',
|
||||
];
|
||||
|
||||
/**
|
||||
* Resolve the element currently presented in fullscreen mode.
|
||||
*
|
||||
* @param {Document} doc Host document reference.
|
||||
* @returns {?Element} Fullscreen element or ``null`` when fullscreen is inactive.
|
||||
*/
|
||||
function getFullscreenElement(doc) {
|
||||
if (!doc) return null;
|
||||
return (
|
||||
doc.fullscreenElement ||
|
||||
doc.webkitFullscreenElement ||
|
||||
doc.mozFullScreenElement ||
|
||||
doc.msFullscreenElement ||
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the container that should host overlays.
|
||||
*
|
||||
* @param {Document} doc Host document reference.
|
||||
* @returns {?Element} Preferred overlay host element.
|
||||
*/
|
||||
function resolveOverlayHost(doc) {
|
||||
const fullscreenElement = getFullscreenElement(doc);
|
||||
if (fullscreenElement && typeof fullscreenElement.appendChild === 'function') {
|
||||
return fullscreenElement;
|
||||
}
|
||||
return doc && doc.body && typeof doc.body.appendChild === 'function' ? doc.body : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update overlay positioning mode based on fullscreen state.
|
||||
*
|
||||
* @param {Element} element Overlay DOM node.
|
||||
* @param {Document} doc Host document reference.
|
||||
* @returns {void}
|
||||
*/
|
||||
function applyOverlayPositioning(element, doc) {
|
||||
if (!element || !element.style) {
|
||||
return;
|
||||
}
|
||||
const fullscreenElement = getFullscreenElement(doc);
|
||||
const desired = fullscreenElement ? 'fixed' : 'absolute';
|
||||
if (element.style.position !== desired) {
|
||||
element.style.position = desired;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a value behaves like a DOM element that can host overlays.
|
||||
@@ -150,6 +205,49 @@ export function createShortInfoOverlayStack(options = {}) {
|
||||
const overlayStates = new Map();
|
||||
const overlayOrder = [];
|
||||
|
||||
/**
|
||||
* Retrieve the active overlay host element.
|
||||
*
|
||||
* @returns {?Element} Host element capable of containing overlays.
|
||||
*/
|
||||
function getOverlayHost() {
|
||||
return resolveOverlayHost(doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Append ``element`` to the preferred overlay host when necessary.
|
||||
*
|
||||
* @param {Element} element Overlay root element.
|
||||
* @returns {void}
|
||||
*/
|
||||
function ensureOverlayAttached(element) {
|
||||
if (!element) return;
|
||||
const host = getOverlayHost();
|
||||
if (!host) return;
|
||||
if (element.parentNode !== host) {
|
||||
host.appendChild(element);
|
||||
}
|
||||
applyOverlayPositioning(element, doc);
|
||||
}
|
||||
|
||||
/**
|
||||
* React to fullscreen transitions by reattaching overlays to the active host.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleFullscreenChange() {
|
||||
for (const state of overlayStates.values()) {
|
||||
ensureOverlayAttached(state.element);
|
||||
}
|
||||
positionAll();
|
||||
}
|
||||
|
||||
if (doc && typeof doc.addEventListener === 'function') {
|
||||
for (const eventName of FULLSCREEN_CHANGE_EVENTS) {
|
||||
doc.addEventListener(eventName, handleFullscreenChange);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an overlay element from the DOM tree.
|
||||
*
|
||||
@@ -215,9 +313,7 @@ export function createShortInfoOverlayStack(options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof doc.body.appendChild === 'function') {
|
||||
doc.body.appendChild(overlayEl);
|
||||
}
|
||||
ensureOverlayAttached(overlayEl);
|
||||
|
||||
state = {
|
||||
anchor,
|
||||
@@ -276,24 +372,27 @@ export function createShortInfoOverlayStack(options = {}) {
|
||||
(win && typeof win.innerHeight === 'number' ? win.innerHeight : 0);
|
||||
const scrollX = (win && typeof win.scrollX === 'number' ? win.scrollX : 0) || 0;
|
||||
const scrollY = (win && typeof win.scrollY === 'number' ? win.scrollY : 0) || 0;
|
||||
const fullscreenElement = getFullscreenElement(doc);
|
||||
const offsetX = fullscreenElement ? 0 : scrollX;
|
||||
const offsetY = fullscreenElement ? 0 : scrollY;
|
||||
|
||||
let left = rect.left + scrollX;
|
||||
let top = rect.top + scrollY;
|
||||
let left = rect.left + offsetX;
|
||||
let top = rect.top + offsetY;
|
||||
|
||||
if (viewportWidth > 0) {
|
||||
const maxLeft = scrollX + viewportWidth - overlayRect.width - 8;
|
||||
left = Math.max(scrollX + 8, Math.min(left, maxLeft));
|
||||
const maxLeft = offsetX + viewportWidth - overlayRect.width - 8;
|
||||
left = Math.max(offsetX + 8, Math.min(left, maxLeft));
|
||||
}
|
||||
if (viewportHeight > 0) {
|
||||
const maxTop = scrollY + viewportHeight - overlayRect.height - 8;
|
||||
top = Math.max(scrollY + 8, Math.min(top, maxTop));
|
||||
const maxTop = offsetY + viewportHeight - overlayRect.height - 8;
|
||||
top = Math.max(offsetY + 8, Math.min(top, maxTop));
|
||||
}
|
||||
|
||||
if (state.element.style) {
|
||||
applyOverlayPositioning(state.element, doc);
|
||||
state.element.style.left = `${left}px`;
|
||||
state.element.style.top = `${top}px`;
|
||||
state.element.style.visibility = 'visible';
|
||||
state.element.style.position = state.element.style.position || 'absolute';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,6 +427,7 @@ export function createShortInfoOverlayStack(options = {}) {
|
||||
if (!state) {
|
||||
return;
|
||||
}
|
||||
ensureOverlayAttached(state.element);
|
||||
if (state.content && typeof state.content.innerHTML === 'string') {
|
||||
state.content.innerHTML = html;
|
||||
}
|
||||
|
||||
@@ -446,7 +446,7 @@ th {
|
||||
line-height: 1.4;
|
||||
min-width: 200px;
|
||||
max-width: 240px;
|
||||
z-index: 2000;
|
||||
z-index: 12000;
|
||||
}
|
||||
|
||||
.short-info-overlay[hidden] {
|
||||
@@ -810,7 +810,7 @@ input[type="radio"] {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--pad);
|
||||
z-index: 4000;
|
||||
z-index: 13000;
|
||||
}
|
||||
|
||||
.info-overlay[hidden] {
|
||||
|
||||
Reference in New Issue
Block a user