Rework the map's filter bar #41
@@ -62,6 +62,7 @@ var mapWindow = window;
|
||||
const mapMarkers = /* @__PURE__ */ new Set();
|
||||
const DEFAULT_MAP_SOURCE_FILTER = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true, sat: false };
|
||||
const mapFilter = { ...DEFAULT_MAP_SOURCE_FILTER };
|
||||
const MAP_FILTER_ALL_KEY = "__all";
|
||||
const mapLocatorFilter = { phase: "band", bands: /* @__PURE__ */ new Set() };
|
||||
let mapSearchFilter = "";
|
||||
let mapRigFilter = "";
|
||||
@@ -838,38 +839,36 @@ var mapWindow = window;
|
||||
container.innerHTML = `<span class="map-locator-empty">No ${kind === "band" ? "bands" : "sources"} available</span>`;
|
||||
return;
|
||||
}
|
||||
let helperText = "";
|
||||
const noun = kind === "band" ? "bands" : "sources";
|
||||
const sourceKeys = kind === "source" ? Object.keys(DEFAULT_MAP_SOURCE_FILTER) : [];
|
||||
const noneSelected = kind === "source" && sourceKeys.every((k) => !mapFilter[k]);
|
||||
if (kind === "source") {
|
||||
if (noneSelected) {
|
||||
helperText = "All sources visible — click to filter";
|
||||
}
|
||||
} else if (!(selectedSet instanceof Set) || selectedSet.size === 0) {
|
||||
helperText = `All ${kind === "band" ? "bands" : "sources"} visible by default`;
|
||||
}
|
||||
const showingAll = kind === "source" ? sourceKeys.every((k) => !mapFilter[k]) : !(selectedSet instanceof Set) || selectedSet.size === 0;
|
||||
const allChip = document.createElement("button");
|
||||
allChip.type = "button";
|
||||
allChip.className = "map-locator-chip map-locator-chip-all";
|
||||
if (showingAll) allChip.classList.add("is-active");
|
||||
allChip.dataset.filterKind = kind;
|
||||
allChip.dataset.filterKey = MAP_FILTER_ALL_KEY;
|
||||
allChip.setAttribute("aria-pressed", showingAll ? "true" : "false");
|
||||
allChip.title = showingAll ? `All ${noun} shown` : `Show all ${noun}`;
|
||||
allChip.innerHTML = `<span class="map-locator-chip-text">All</span>`;
|
||||
container.appendChild(allChip);
|
||||
for (const item of items) {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "map-locator-chip";
|
||||
const isActive = kind === "source" ? !!mapFilter[item.key] : !!selectedSet?.has(item.key);
|
||||
if (kind === "source" && noneSelected) {
|
||||
if (showingAll) {
|
||||
btn.classList.add("is-default");
|
||||
} else if (!isActive) {
|
||||
btn.classList.add("is-inactive");
|
||||
}
|
||||
btn.setAttribute("aria-pressed", !showingAll && isActive ? "true" : "false");
|
||||
btn.dataset.filterKind = kind;
|
||||
btn.dataset.filterKey = item.key;
|
||||
btn.style.setProperty("--chip-color", item.color);
|
||||
btn.innerHTML = `<span class="map-locator-chip-text">${escapeMapHtml(item.label)}</span>`;
|
||||
container.appendChild(btn);
|
||||
}
|
||||
if (helperText) {
|
||||
const hint = document.createElement("span");
|
||||
hint.className = "map-locator-empty";
|
||||
hint.textContent = helperText;
|
||||
container.appendChild(hint);
|
||||
}
|
||||
}
|
||||
function renderMapLocatorPhaseRow(container, phase) {
|
||||
if (!container) return;
|
||||
@@ -977,11 +976,10 @@ var mapWindow = window;
|
||||
renderMapLocatorLegend(mapLocatorFilter.phase, sourceItems, bandItems);
|
||||
if (!phaseEl || !choiceEl || !choiceLabelEl) return;
|
||||
renderMapLocatorPhaseRow(phaseEl, mapLocatorFilter.phase);
|
||||
choiceLabelEl.textContent = "Show";
|
||||
if (mapLocatorFilter.phase === "band") {
|
||||
choiceLabelEl.textContent = "Visible Bands";
|
||||
renderMapLocatorChipRow(choiceEl, bandItems, mapLocatorFilter.bands, "band");
|
||||
} else {
|
||||
choiceLabelEl.textContent = "Visible Sources";
|
||||
renderMapLocatorChipRow(choiceEl, sourceItems, null, "source");
|
||||
}
|
||||
syncLocatorMarkerStyles();
|
||||
@@ -1533,7 +1531,13 @@ var mapWindow = window;
|
||||
const kind = String(chip.dataset.filterKind || "");
|
||||
const key = String(chip.dataset.filterKey || "");
|
||||
if (!key) return;
|
||||
if (kind === "source" && Object.prototype.hasOwnProperty.call(mapFilter, key)) {
|
||||
if (key === MAP_FILTER_ALL_KEY) {
|
||||
if (kind === "source") {
|
||||
for (const srcKey of Object.keys(DEFAULT_MAP_SOURCE_FILTER)) mapFilter[srcKey] = false;
|
||||
} else {
|
||||
mapLocatorFilter.bands.clear();
|
||||
}
|
||||
} else if (kind === "source" && Object.prototype.hasOwnProperty.call(mapFilter, key)) {
|
||||
const sourceKey = key;
|
||||
mapFilter[sourceKey] = !mapFilter[sourceKey];
|
||||
const srcKeys = Object.keys(DEFAULT_MAP_SOURCE_FILTER);
|
||||
@@ -2148,14 +2152,16 @@ var mapWindow = window;
|
||||
function updateMapContactPathsToggle() {
|
||||
const btn = mapEl("map-contact-paths-toggle");
|
||||
if (!btn) return;
|
||||
btn.textContent = mapDecodeContactPathsEnabled ? "Contact Paths On" : "Contact Paths Off";
|
||||
btn.classList.toggle("is-active", mapDecodeContactPathsEnabled);
|
||||
btn.setAttribute("aria-pressed", mapDecodeContactPathsEnabled ? "true" : "false");
|
||||
btn.title = mapDecodeContactPathsEnabled ? "Directed decode paths are drawn when the target locator is known" : "Directed decode paths are hidden";
|
||||
}
|
||||
function updateMapP2pPathsToggle() {
|
||||
const btn = mapEl("map-p2p-paths-toggle");
|
||||
if (!btn) return;
|
||||
btn.textContent = mapP2pRadioPathsEnabled ? "TRX Paths On" : "TRX Paths Off";
|
||||
btn.classList.toggle("is-active", mapP2pRadioPathsEnabled);
|
||||
btn.setAttribute("aria-pressed", mapP2pRadioPathsEnabled ? "true" : "false");
|
||||
btn.title = mapP2pRadioPathsEnabled ? "TRX paths are drawn from a station popup" : "TRX paths are hidden";
|
||||
}
|
||||
function scheduleDecodeMapMaintenance() {
|
||||
if (C.decodeHistoryMapRenderingDeferred()) {
|
||||
|
||||
@@ -1003,7 +1003,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<div class="map-overlay-panel">
|
||||
<div class="map-overlay-filters">
|
||||
<div class="map-locator-filter-group">
|
||||
<span class="map-locator-filter-label">Filter by</span>
|
||||
<span class="map-locator-filter-label">Filter</span>
|
||||
<div id="map-locator-phase" class="map-locator-phase-row"></div>
|
||||
</div>
|
||||
<div class="map-locator-filter-group">
|
||||
@@ -1016,10 +1016,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<option value="">All</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="map-locator-filter-group map-filter-grow">
|
||||
<span class="map-locator-filter-label">Search</span>
|
||||
<input type="text" id="map-search-filter" class="map-search-input" placeholder="Callsign, MMSI, locator, message..." />
|
||||
</div>
|
||||
<div class="map-locator-filter-group">
|
||||
<span class="map-locator-filter-label">History</span>
|
||||
<select id="map-history-limit" class="map-history-select" aria-label="Map history limit">
|
||||
@@ -1035,11 +1031,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<div class="map-locator-filter-group">
|
||||
<span class="map-locator-filter-label">Paths</span>
|
||||
<div class="map-locator-phase-row">
|
||||
<button type="button" id="map-p2p-paths-toggle" class="map-locator-phase-btn" title="TRX paths are drawn from a station popup">TRX Paths On</button>
|
||||
<button type="button" id="map-contact-paths-toggle" class="map-locator-phase-btn" title="Directed decode paths are drawn when the target locator is known">Contact Paths On</button>
|
||||
<button type="button" id="map-p2p-paths-toggle" class="map-locator-phase-btn" aria-pressed="true" title="TRX paths are drawn from a station popup">TRX</button>
|
||||
<button type="button" id="map-contact-paths-toggle" class="map-locator-phase-btn" aria-pressed="true" title="Directed decode paths are drawn when the target locator is known">Contact</button>
|
||||
<span class="map-paths-hint">TRX paths on popup, directed decode paths when target locator is known</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Last, so the search field takes whatever the fixed-width
|
||||
groups leave on the bar's final row rather than a sliver. -->
|
||||
<div class="map-locator-filter-group map-filter-grow">
|
||||
<span class="map-locator-filter-label">Search</span>
|
||||
<input type="text" id="map-search-filter" class="map-search-input" placeholder="Callsign, MMSI, locator, message..." />
|
||||
</div>
|
||||
</div>
|
||||
<div class="map-overlay-actions">
|
||||
<button type="button" id="map-fullscreen-btn" class="map-fullscreen-btn">Fullscreen</button>
|
||||
|
||||
@@ -2570,7 +2570,10 @@ button.map-qso-card:focus-visible {
|
||||
z-index: 410;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
/* Stretched, not centred: the rule dividing the filters from the buttons is
|
||||
drawn on the button block's edge, and it has to run the height of the bar
|
||||
rather than float beside it as a stub. */
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
width: auto;
|
||||
max-height: calc(100% - 1.4rem);
|
||||
@@ -2590,6 +2593,9 @@ button.map-qso-card:focus-visible {
|
||||
flex: 1 1 auto;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
/* Rows keep their own height when the bar is taller than they are, so the
|
||||
groups stay a bar's two rows rather than drifting apart to fill it. */
|
||||
align-content: center;
|
||||
gap: 0.35rem 0.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
@@ -2603,6 +2609,9 @@ button.map-qso-card:focus-visible {
|
||||
.map-overlay-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
/* The block spans the bar so its divider can; the buttons still sit level
|
||||
with the middle of it. */
|
||||
align-self: stretch;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
@@ -2625,17 +2634,29 @@ button.map-qso-card:focus-visible {
|
||||
padding-right: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
/* The search field takes whatever room the fixed-width groups leave. */
|
||||
/* The search field takes whatever room the fixed-width groups leave, up to a
|
||||
width past which a text box spanning the map reads as a mistake. */
|
||||
.map-overlay-panel .map-filter-grow {
|
||||
flex: 1 1 9rem;
|
||||
max-width: 26rem;
|
||||
}
|
||||
/* One gutter for every label, so whichever group starts a row starts it in the
|
||||
same place: at their natural widths the labels staggered each row's first
|
||||
control by however much the label above it was wider or narrower. */
|
||||
.map-overlay-panel .map-locator-filter-label {
|
||||
min-width: 0;
|
||||
min-width: 3.5rem;
|
||||
padding-top: 0;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.02em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* The two rows of the bar are led by the two pairs of phase buttons, and
|
||||
SOURCE|BAND is wider than TRX|CONTACT: left to their own widths the groups
|
||||
after them missed each other by four pixels. One width for both pairs. */
|
||||
.map-overlay-panel .map-locator-phase-row {
|
||||
flex: 0 0 auto;
|
||||
min-width: 9rem;
|
||||
}
|
||||
.map-overlay-panel .map-history-select {
|
||||
flex: 0 0 auto;
|
||||
width: auto;
|
||||
@@ -3471,6 +3492,23 @@ body.map-fake-fullscreen-active {
|
||||
border-color: color-mix(in srgb, var(--border-light) 68%, transparent);
|
||||
background: color-mix(in srgb, var(--input-bg) 96%, transparent);
|
||||
}
|
||||
/* "All" clears the selection rather than naming a band or a source, so it
|
||||
borrows the phase buttons' look instead of a colour of its own — and says
|
||||
in one chip's width what a line of helper text used to say in the bar. */
|
||||
.map-locator-chip-all {
|
||||
--chip-color: var(--border-light);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.map-locator-chip-all.is-active {
|
||||
border-color: var(--accent-green);
|
||||
background: color-mix(in srgb, var(--accent-green) 10%, var(--input-bg));
|
||||
color: var(--accent-green);
|
||||
}
|
||||
.map-locator-chip-all .map-locator-chip-text {
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.03em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.map-locator-chip-text {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
@@ -4196,9 +4234,23 @@ body[data-operator-layout="broadcast"] #cw-bar-overlay {
|
||||
padding-right: 0;
|
||||
border-right: 0;
|
||||
}
|
||||
/* Stacked in a column the panel scrolls, and the search field is no use at
|
||||
the bottom of it: it goes back to the top, where it needs no scrolling. */
|
||||
.map-overlay-panel .map-filter-grow {
|
||||
order: -1;
|
||||
max-width: none;
|
||||
}
|
||||
.map-overlay-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
/* Stacked, the buttons sit under the filters rather than beside them, so the
|
||||
rule that divides them has to lie across the panel, not down its left. */
|
||||
.map-overlay-panel:not(.filters-hidden) .map-overlay-actions {
|
||||
padding-left: 0;
|
||||
padding-top: 0.5rem;
|
||||
border-left: 0;
|
||||
border-top: 1px solid color-mix(in srgb, var(--border-light) 55%, transparent);
|
||||
}
|
||||
.map-overlay-panel .map-paths-hint {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
|
||||
@@ -230,6 +230,8 @@ const mapWindow = window as unknown as MapWindow;
|
||||
const mapMarkers = new Set<TrxLayer>();
|
||||
const DEFAULT_MAP_SOURCE_FILTER: Record<MapFilterKey, boolean> = { ais: true, vdes: true, aprs: true, bookmark: false, ft8: true, ft4: true, ft2: true, wspr: true, sat: false };
|
||||
const mapFilter: Record<MapFilterKey, boolean> = { ...DEFAULT_MAP_SOURCE_FILTER };
|
||||
/** Chip key that clears a selection rather than naming a band or a source. */
|
||||
const MAP_FILTER_ALL_KEY = "__all";
|
||||
const mapLocatorFilter: { phase: "band" | "type"; bands: Set<string> } = { phase: "band", bands: new Set() };
|
||||
let mapSearchFilter = "";
|
||||
let mapRigFilter = ""; // "" = all rigs
|
||||
@@ -1079,38 +1081,42 @@ const mapWindow = window as unknown as MapWindow;
|
||||
container.innerHTML = `<span class="map-locator-empty">No ${kind === "band" ? "bands" : "sources"} available</span>`;
|
||||
return;
|
||||
}
|
||||
let helperText = "";
|
||||
const noun = kind === "band" ? "bands" : "sources";
|
||||
const sourceKeys = kind === "source" ? Object.keys(DEFAULT_MAP_SOURCE_FILTER) as MapFilterKey[] : [];
|
||||
const noneSelected = kind === "source" && sourceKeys.every((k) => !mapFilter[k]);
|
||||
if (kind === "source") {
|
||||
if (noneSelected) {
|
||||
helperText = "All sources visible \u2014 click to filter";
|
||||
}
|
||||
} else if (!(selectedSet instanceof Set) || selectedSet.size === 0) {
|
||||
helperText = `All ${kind === "band" ? "bands" : "sources"} visible by default`;
|
||||
}
|
||||
// Selecting nothing selects everything, for both kinds.
|
||||
const showingAll = kind === "source"
|
||||
? sourceKeys.every((k) => !mapFilter[k])
|
||||
: !(selectedSet instanceof Set) || selectedSet.size === 0;
|
||||
// An "All" chip carries what a sentence of helper text used to say, in a
|
||||
// width the bar can afford, and gives the selection somewhere to be undone.
|
||||
const allChip = document.createElement("button");
|
||||
allChip.type = "button";
|
||||
allChip.className = "map-locator-chip map-locator-chip-all";
|
||||
if (showingAll) allChip.classList.add("is-active");
|
||||
allChip.dataset.filterKind = kind;
|
||||
allChip.dataset.filterKey = MAP_FILTER_ALL_KEY;
|
||||
allChip.setAttribute("aria-pressed", showingAll ? "true" : "false");
|
||||
allChip.title = showingAll ? `All ${noun} shown` : `Show all ${noun}`;
|
||||
allChip.innerHTML = `<span class="map-locator-chip-text">All</span>`;
|
||||
container.appendChild(allChip);
|
||||
for (const item of items) {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "map-locator-chip";
|
||||
const isActive = kind === "source" ? !!mapFilter[item.key as MapFilterKey] : !!selectedSet?.has(item.key);
|
||||
if (kind === "source" && noneSelected) {
|
||||
// Nothing is filtered out yet, so no chip is dimmed as if it were.
|
||||
if (showingAll) {
|
||||
btn.classList.add("is-default");
|
||||
} else if (!isActive) {
|
||||
btn.classList.add("is-inactive");
|
||||
}
|
||||
btn.setAttribute("aria-pressed", !showingAll && isActive ? "true" : "false");
|
||||
btn.dataset.filterKind = kind;
|
||||
btn.dataset.filterKey = item.key;
|
||||
btn.style.setProperty("--chip-color", item.color);
|
||||
btn.innerHTML = `<span class="map-locator-chip-text">${escapeMapHtml(item.label)}</span>`;
|
||||
container.appendChild(btn);
|
||||
}
|
||||
if (helperText) {
|
||||
const hint = document.createElement("span");
|
||||
hint.className = "map-locator-empty";
|
||||
hint.textContent = helperText;
|
||||
container.appendChild(hint);
|
||||
}
|
||||
}
|
||||
|
||||
function renderMapLocatorPhaseRow(container: HTMLElement, phase: "band" | "type"): void {
|
||||
@@ -1234,11 +1240,12 @@ const mapWindow = window as unknown as MapWindow;
|
||||
if (!phaseEl || !choiceEl || !choiceLabelEl) return;
|
||||
|
||||
renderMapLocatorPhaseRow(phaseEl, mapLocatorFilter.phase);
|
||||
// The phase buttons next door already name the dimension; "Show" is the
|
||||
// rest of the sentence, and it keeps the bar's labels a uniform width.
|
||||
choiceLabelEl.textContent = "Show";
|
||||
if (mapLocatorFilter.phase === "band") {
|
||||
choiceLabelEl.textContent = "Visible Bands";
|
||||
renderMapLocatorChipRow(choiceEl, bandItems, mapLocatorFilter.bands, "band");
|
||||
} else {
|
||||
choiceLabelEl.textContent = "Visible Sources";
|
||||
renderMapLocatorChipRow(choiceEl, sourceItems, null, "source");
|
||||
}
|
||||
syncLocatorMarkerStyles();
|
||||
@@ -1863,7 +1870,14 @@ const mapWindow = window as unknown as MapWindow;
|
||||
const kind = String(chip.dataset.filterKind || "");
|
||||
const key = String(chip.dataset.filterKey || "");
|
||||
if (!key) return;
|
||||
if (kind === "source" && Object.prototype.hasOwnProperty.call(mapFilter, key)) {
|
||||
if (key === MAP_FILTER_ALL_KEY) {
|
||||
// Back to no selection at all, which is what shows everything.
|
||||
if (kind === "source") {
|
||||
for (const srcKey of Object.keys(DEFAULT_MAP_SOURCE_FILTER) as MapFilterKey[]) mapFilter[srcKey] = false;
|
||||
} else {
|
||||
mapLocatorFilter.bands.clear();
|
||||
}
|
||||
} else if (kind === "source" && Object.prototype.hasOwnProperty.call(mapFilter, key)) {
|
||||
// toggle the clicked source; when none are selected everything is shown
|
||||
const sourceKey = key as MapFilterKey;
|
||||
mapFilter[sourceKey] = !mapFilter[sourceKey];
|
||||
@@ -2584,18 +2598,26 @@ const mapWindow = window as unknown as MapWindow;
|
||||
syncDecodeContactPathVisibility();
|
||||
}
|
||||
|
||||
// The buttons light up when they are on, so the label need not repeat it —
|
||||
// an "On"/"Off" suffix on each cost the bar most of a row.
|
||||
function updateMapContactPathsToggle() {
|
||||
const btn = mapEl("map-contact-paths-toggle");
|
||||
if (!btn) return;
|
||||
btn.textContent = mapDecodeContactPathsEnabled ? "Contact Paths On" : "Contact Paths Off";
|
||||
btn.classList.toggle("is-active", mapDecodeContactPathsEnabled);
|
||||
btn.setAttribute("aria-pressed", mapDecodeContactPathsEnabled ? "true" : "false");
|
||||
btn.title = mapDecodeContactPathsEnabled
|
||||
? "Directed decode paths are drawn when the target locator is known"
|
||||
: "Directed decode paths are hidden";
|
||||
}
|
||||
|
||||
function updateMapP2pPathsToggle() {
|
||||
const btn = mapEl("map-p2p-paths-toggle");
|
||||
if (!btn) return;
|
||||
btn.textContent = mapP2pRadioPathsEnabled ? "TRX Paths On" : "TRX Paths Off";
|
||||
btn.classList.toggle("is-active", mapP2pRadioPathsEnabled);
|
||||
btn.setAttribute("aria-pressed", mapP2pRadioPathsEnabled ? "true" : "false");
|
||||
btn.title = mapP2pRadioPathsEnabled
|
||||
? "TRX paths are drawn from a station popup"
|
||||
: "TRX paths are hidden";
|
||||
}
|
||||
|
||||
function scheduleDecodeMapMaintenance() {
|
||||
|
||||
@@ -12,7 +12,7 @@ import assert from "node:assert/strict";
|
||||
import { chromium } from "playwright-core";
|
||||
import { startBrowser, startWebFixture } from "./web-fixture.mjs";
|
||||
|
||||
/* global document, getComputedStyle */
|
||||
/* global document, getComputedStyle, window */
|
||||
|
||||
const BOOKMARKS = [
|
||||
{ id: "b1", name: "40m FT8", freq_hz: 7074000, mode: "DIG", category: "Digital", comment: "", locator: "" },
|
||||
@@ -257,6 +257,24 @@ try {
|
||||
// Both controls belong to the bar now, not to a floating corner block.
|
||||
actionsInBar: [...panel.querySelectorAll(".map-overlay-actions button")]
|
||||
.map((button) => button.id).join(","),
|
||||
// The rule dividing them from the filters is drawn on this block's
|
||||
// edge, so the block has to run the height of the filters beside it —
|
||||
// centred, it left the rule floating as a stub against a two-row bar.
|
||||
actionsShort: Math.round(box(panel.querySelector(".map-overlay-filters")).height
|
||||
- box(panel.querySelector(".map-overlay-actions")).height),
|
||||
// Every label sits in a gutter of one width, so whichever group leads
|
||||
// a row leads it from the same place: with the labels at their natural
|
||||
// widths, a row starting "Search" began 10px off one starting "Filter".
|
||||
rowStarts: (() => {
|
||||
const leaders = new Map();
|
||||
for (const group of panel.querySelectorAll(".map-overlay-filters .map-locator-filter-group")) {
|
||||
const rect = box(group);
|
||||
// Groups of a row differ in height, so bucket them by their middle.
|
||||
const row = Math.round((rect.top + rect.height / 2) / 20);
|
||||
if (!leaders.has(row) || rect.left < box(leaders.get(row)).left) leaders.set(row, group);
|
||||
}
|
||||
return [...leaders.values()].map((group) => Math.round(box(group.children[1]).left));
|
||||
})(),
|
||||
clipped: panel.scrollWidth > panel.clientWidth + 1 || panel.scrollHeight > panel.clientHeight + 1,
|
||||
};
|
||||
});
|
||||
@@ -266,10 +284,66 @@ try {
|
||||
assert.equal(bar.hitsZoom, false, `the filter bar covers the zoom buttons at ${width}px`);
|
||||
assert.equal(bar.actionsInBar, "map-fullscreen-btn,map-overlay-toggle-btn",
|
||||
`the bar carries "${bar.actionsInBar}" at ${width}px`);
|
||||
assert.ok(bar.actionsShort <= 1,
|
||||
`the buttons' divider falls ${bar.actionsShort}px short of the bar at ${width}px`);
|
||||
assert.equal(new Set(bar.rowStarts).size, 1,
|
||||
`the bar's rows start at ${bar.rowStarts.join(", ")}px at ${width}px`);
|
||||
assert.equal(bar.hitsLegend, false, `the filter bar covers the band legend at ${width}px`);
|
||||
assert.equal(bar.clipped, false, `the filter bar is clipping its own controls at ${width}px`);
|
||||
}
|
||||
|
||||
// Band chips only exist once something has been heard on a band. The bar
|
||||
// used to explain "all bands visible by default" in a line of prose wedged
|
||||
// between the chips and the next group, which is neither what a toolbar is
|
||||
// for nor a width it can spare: an "All" chip says it and undoes a
|
||||
// selection. Nothing is dimmed while nothing is filtered out, either — every
|
||||
// chip used to come up greyed at the very moment all of them were showing.
|
||||
await mapView.page.evaluate(() => {
|
||||
const ts = Date.now();
|
||||
for (const [grid, hz] of [["JO94", 14_074_000], ["JN48", 7_074_000], ["FN42", 21_074_000]]) {
|
||||
window.trxPluginRuntime.dispatch("ft8",
|
||||
{ message: `CQ TEST ${grid}`, grid, freq_hz: hz, snr_db: -8, ts_ms: ts, rig_id: "rig-a" });
|
||||
}
|
||||
});
|
||||
await mapView.page.waitForTimeout(1500);
|
||||
const chipRow = () => mapView.page.evaluate(() => {
|
||||
const row = document.getElementById("map-locator-choice-filter");
|
||||
const chips = [...row.querySelectorAll(".map-locator-chip")];
|
||||
const all = row.querySelector(".map-locator-chip-all");
|
||||
return {
|
||||
bands: chips.filter((chip) => chip !== all).map((chip) => chip.textContent.trim()),
|
||||
prose: row.querySelector(".map-locator-empty")?.textContent ?? null,
|
||||
allActive: all?.classList.contains("is-active") ?? null,
|
||||
dimmed: chips.filter((chip) => chip.classList.contains("is-inactive"))
|
||||
.map((chip) => chip.textContent.trim()),
|
||||
selected: chips.filter((chip) => chip.getAttribute("aria-pressed") === "true"
|
||||
&& chip !== all).map((chip) => chip.textContent.trim()),
|
||||
};
|
||||
});
|
||||
|
||||
const unfiltered = await chipRow();
|
||||
assert.ok(unfiltered.bands.includes("20m") && unfiltered.bands.includes("40m"),
|
||||
`the chip row is showing ${unfiltered.bands.join(",")}`);
|
||||
assert.equal(unfiltered.prose, null, `the bar is explaining itself in prose: "${unfiltered.prose}"`);
|
||||
assert.equal(unfiltered.allActive, true, "All is not lit while every band is on the map");
|
||||
assert.deepEqual(unfiltered.dimmed, [], `${unfiltered.dimmed.join(",")} came up dimmed with no filter set`);
|
||||
|
||||
await mapView.page.locator('#map-locator-choice-filter .map-locator-chip[data-filter-key="20m"]').click();
|
||||
await mapView.page.waitForTimeout(300);
|
||||
const filtered = await chipRow();
|
||||
assert.deepEqual(filtered.selected, ["20m"], `picking 20m selected ${filtered.selected.join(",")}`);
|
||||
assert.equal(filtered.allActive, false, "All stayed lit with a band picked out");
|
||||
assert.ok(filtered.dimmed.includes("40m"), "the bands now filtered out are not shown as such");
|
||||
|
||||
// And back: All is how a selection is undone without hunting for the chips
|
||||
// that are in it.
|
||||
await mapView.page.locator('#map-locator-choice-filter .map-locator-chip-all').click();
|
||||
await mapView.page.waitForTimeout(300);
|
||||
const cleared = await chipRow();
|
||||
assert.equal(cleared.allActive, true, "All did not clear the band selection");
|
||||
assert.deepEqual(cleared.selected, [], `${cleared.selected.join(",")} survived All`);
|
||||
assert.deepEqual(cleared.dimmed, [], `${cleared.dimmed.join(",")} stayed dimmed after All`);
|
||||
|
||||
// Hiding gives the map back, but leaves the bar itself — collapsed to its
|
||||
// two controls at the right-hand edge — so the filters can be brought back.
|
||||
await mapView.page.locator("#map-overlay-toggle-btn").click();
|
||||
|
||||
Reference in New Issue
Block a user