feat: add flyout menu open/close/position base, replace old tile-delete handler
This commit is contained in:
+111
-8
@@ -132,6 +132,13 @@ let _previewTimer = null;
|
|||||||
|
|
||||||
var _previewSessionName = null; // track by NAME, not DOM element
|
var _previewSessionName = null; // track by NAME, not DOM element
|
||||||
|
|
||||||
|
// Flyout menu state
|
||||||
|
let _flyoutMenuEl = null;
|
||||||
|
let _flyoutSubmenuEl = null;
|
||||||
|
let _flyoutSessionKey = null;
|
||||||
|
let _flyoutSessionName = null;
|
||||||
|
let _flyoutRemoteId = null;
|
||||||
|
|
||||||
// ─── Settings state ───────────────────────────────────────────────────────────
|
// ─── Settings state ───────────────────────────────────────────────────────────
|
||||||
let _settingsOpen = false;
|
let _settingsOpen = false;
|
||||||
let _serverSettings = null;
|
let _serverSettings = null;
|
||||||
@@ -1453,6 +1460,103 @@ function hidePreview() {
|
|||||||
_previewSessionName = null;
|
_previewSessionName = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tile Flyout Menu ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the flyout menu for a session tile's ⋮ button.
|
||||||
|
* Creates a floating menu appended to document.body, positioned relative to
|
||||||
|
* the trigger button via getBoundingClientRect. On mobile, renders as a
|
||||||
|
* bottom action sheet instead.
|
||||||
|
* @param {HTMLElement} triggerEl - The .tile-options-btn element that was clicked
|
||||||
|
*/
|
||||||
|
function openFlyoutMenu(triggerEl) {
|
||||||
|
closeFlyoutMenu();
|
||||||
|
|
||||||
|
// Read session info from the tile
|
||||||
|
var tile = triggerEl.closest('[data-session-key]');
|
||||||
|
if (!tile) return;
|
||||||
|
_flyoutSessionKey = tile.dataset.sessionKey || '';
|
||||||
|
_flyoutSessionName = tile.dataset.session || '';
|
||||||
|
_flyoutRemoteId = tile.dataset.remoteId || '';
|
||||||
|
|
||||||
|
if (isMobile()) {
|
||||||
|
_openFlyoutSheet();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build menu items based on active view type
|
||||||
|
var menuHtml = _buildFlyoutMenuItems();
|
||||||
|
|
||||||
|
var menu = document.createElement('div');
|
||||||
|
menu.className = 'flyout-menu';
|
||||||
|
menu.setAttribute('role', 'menu');
|
||||||
|
menu.setAttribute('aria-label', 'Session options');
|
||||||
|
menu.innerHTML = menuHtml;
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
_flyoutMenuEl = menu;
|
||||||
|
|
||||||
|
// Position relative to trigger
|
||||||
|
var rect = triggerEl.getBoundingClientRect();
|
||||||
|
var menuWidth = menu.offsetWidth;
|
||||||
|
var menuHeight = menu.offsetHeight;
|
||||||
|
|
||||||
|
// Default: below and to the left of the trigger
|
||||||
|
var top = rect.bottom + 4;
|
||||||
|
var left = rect.right - menuWidth;
|
||||||
|
|
||||||
|
// Keep within viewport
|
||||||
|
if (left < 8) left = 8;
|
||||||
|
if (top + menuHeight > window.innerHeight - 8) {
|
||||||
|
top = rect.top - menuHeight - 4;
|
||||||
|
}
|
||||||
|
if (top < 8) top = 8;
|
||||||
|
|
||||||
|
menu.style.top = top + 'px';
|
||||||
|
menu.style.left = left + 'px';
|
||||||
|
|
||||||
|
// Delegated click handler on the flyout
|
||||||
|
menu.addEventListener('click', _handleFlyoutClick);
|
||||||
|
|
||||||
|
// Close on click-outside (next tick to avoid the opening click)
|
||||||
|
setTimeout(function() {
|
||||||
|
document.addEventListener('click', _flyoutOutsideClickHandler, true);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the flyout menu and any open submenu.
|
||||||
|
*/
|
||||||
|
function closeFlyoutMenu() {
|
||||||
|
if (_flyoutSubmenuEl) {
|
||||||
|
_flyoutSubmenuEl.remove();
|
||||||
|
_flyoutSubmenuEl = null;
|
||||||
|
}
|
||||||
|
if (_flyoutMenuEl) {
|
||||||
|
_flyoutMenuEl.removeEventListener('click', _handleFlyoutClick);
|
||||||
|
_flyoutMenuEl.remove();
|
||||||
|
_flyoutMenuEl = null;
|
||||||
|
}
|
||||||
|
// Remove mobile sheet if open
|
||||||
|
var sheet = document.querySelector('.flyout-sheet');
|
||||||
|
if (sheet) sheet.remove();
|
||||||
|
|
||||||
|
document.removeEventListener('click', _flyoutOutsideClickHandler, true);
|
||||||
|
_flyoutSessionKey = null;
|
||||||
|
_flyoutSessionName = null;
|
||||||
|
_flyoutRemoteId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Click-outside handler for the flyout menu.
|
||||||
|
* @param {MouseEvent} e
|
||||||
|
*/
|
||||||
|
function _flyoutOutsideClickHandler(e) {
|
||||||
|
if (_flyoutMenuEl && !_flyoutMenuEl.contains(e.target) &&
|
||||||
|
(!_flyoutSubmenuEl || !_flyoutSubmenuEl.contains(e.target))) {
|
||||||
|
closeFlyoutMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Notification permission ────────────────────────────────────────────────
|
// ─── Notification permission ────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2776,16 +2880,12 @@ function killSession(name, remoteId) {
|
|||||||
* Called once after restoreState() resolves.
|
* Called once after restoreState() resolves.
|
||||||
*/
|
*/
|
||||||
function bindStaticEventListeners() {
|
function bindStaticEventListeners() {
|
||||||
// Delegated kill-session handler (tiles + sidebar items are re-rendered each poll)
|
// Delegated ⋮ options button handler (tiles are re-rendered each poll)
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
var deleteBtn = e.target.closest && e.target.closest('.tile-delete, .sidebar-delete');
|
var optionsBtn = e.target.closest && e.target.closest('.tile-options-btn');
|
||||||
if (!deleteBtn) return;
|
if (!optionsBtn) return;
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
var name = deleteBtn.dataset.session;
|
openFlyoutMenu(optionsBtn);
|
||||||
// Walk up to the tile/sidebar-item to get remoteId for federation routing
|
|
||||||
var container = deleteBtn.closest('[data-remote-id]');
|
|
||||||
var remoteId = container ? container.dataset.remoteId : '';
|
|
||||||
if (name) killSession(name, remoteId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
on($('back-btn'), 'click', closeSession);
|
on($('back-btn'), 'click', closeSession);
|
||||||
@@ -3247,6 +3347,9 @@ if (typeof module !== 'undefined' && module.exports) {
|
|||||||
createNewSession,
|
createNewSession,
|
||||||
// Kill session
|
// Kill session
|
||||||
killSession,
|
killSession,
|
||||||
|
// Flyout menu
|
||||||
|
openFlyoutMenu,
|
||||||
|
closeFlyoutMenu,
|
||||||
// Filter bar
|
// Filter bar
|
||||||
renderFilterBar,
|
renderFilterBar,
|
||||||
// View dropdown
|
// View dropdown
|
||||||
|
|||||||
@@ -3709,3 +3709,42 @@ def test_tile_delete_button_removed() -> None:
|
|||||||
assert "tile-delete" not in fn_body, (
|
assert "tile-delete" not in fn_body, (
|
||||||
"buildTileHTML must not render the old .tile-delete button (kill moved to flyout)"
|
"buildTileHTML must not render the old .tile-delete button (kill moved to flyout)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_open_flyout_menu_function_exists() -> None:
|
||||||
|
"""app.js must define an openFlyoutMenu function."""
|
||||||
|
assert "function openFlyoutMenu" in _JS, (
|
||||||
|
"app.js must contain an openFlyoutMenu function"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_close_flyout_menu_function_exists() -> None:
|
||||||
|
"""app.js must define a closeFlyoutMenu function."""
|
||||||
|
assert "function closeFlyoutMenu" in _JS, (
|
||||||
|
"app.js must contain a closeFlyoutMenu function"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_flyout_menu_uses_fixed_positioning() -> None:
|
||||||
|
"""openFlyoutMenu must use position:fixed and getBoundingClientRect for positioning."""
|
||||||
|
fn_body = _JS.split("function openFlyoutMenu")[1].split("\nfunction ")[0]
|
||||||
|
assert "getBoundingClientRect" in fn_body, (
|
||||||
|
"openFlyoutMenu must use getBoundingClientRect to calculate position"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_flyout_delegated_on_tile_container() -> None:
|
||||||
|
"""A delegated click listener must handle .tile-options-btn clicks."""
|
||||||
|
assert "tile-options-btn" in _JS.split("bindStaticEventListeners")[1], (
|
||||||
|
"bindStaticEventListeners must handle .tile-options-btn clicks via delegation"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_old_tile_delete_handler_removed() -> None:
|
||||||
|
"""The old delegated .tile-delete click handler must be removed."""
|
||||||
|
bind_body = _JS.split("function bindStaticEventListeners")[1].split("\nfunction ")[
|
||||||
|
0
|
||||||
|
]
|
||||||
|
assert "tile-delete" not in bind_body, (
|
||||||
|
"The old .tile-delete delegated handler must be removed from bindStaticEventListeners"
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user