Replace pages' use of the options call with directly building the HTML components using templates, as the syntax is much nicer. Use a common set of data provided to all modules via pagetemplate.py instead of passing things like web_ui_options around. Closes #153

This commit is contained in:
Ian Renton
2026-09-25 21:22:31 +01:00
parent 4df00c321c
commit 8de163469b
27 changed files with 493 additions and 534 deletions
+14 -47
View File
@@ -26,45 +26,8 @@ const PROVIDER_CREDENTIAL_SCHEMAS = {
]
};
// Load server options. Once a successful callback is made from this, we can populate the choice boxes in the form and load
// any saved values from local storage.
function loadOptions() {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
// Populate modes drop-down
$.each(options["modes"], function (i, m) {
$('#mode').append($('<option>', {
value: m,
text: m
}));
});
// Populate activity drop-down
$.each(options["activities"], function (i, activity) {
$('#activity').append($('<option>', {
value: activity.name,
text: activity.name
}));
});
// Load reCAPTCHA if a site key is configured (key is inlined into page by server)
if (window._recaptchaSiteKey) {
loadRecaptcha(window._recaptchaSiteKey);
}
// Load settings from settings storage now all the controls are available
loadSettings();
// Update the upstream area for any pre-selected activity
updateUpstreamArea();
});
}
// Load and inject the reCAPTCHA script
function loadRecaptcha(siteKey) {
window._recaptchaSiteKey = siteKey;
function loadRecaptcha() {
if (!document.getElementById('recaptcha-script')) {
const script = document.createElement('script');
script.id = 'recaptcha-script';
@@ -79,20 +42,19 @@ function loadRecaptcha(siteKey) {
// Called by reCAPTCHA after its script loads
function renderRecaptcha() {
window._recaptchaWidgetId = grecaptcha.render('recaptcha-widget', {
sitekey: window._recaptchaSiteKey,
sitekey: RECAPTCHA_SITE_KEY,
size: 'normal'
});
}
// Update the "Send spot to..." area based on the currently selected activity
function updateUpstreamArea() {
if (!window._allowUpstreamSpotting || !options || !options["spot_submit_providers"]) {
if (!ALLOW_UPSTREAM_SPOTTING) {
$("#upstream-area").hide();
return;
}
const activity = $("#activity").val();
const providers = (activity && options["spot_submit_providers"][activity]) ? options["spot_submit_providers"][activity] : [];
const providers = SPOT_SUBMIT_PROVIDERS[$("#activity").val()] || [];
if (providers.length === 0) {
$("#upstream-area").hide();
@@ -131,9 +93,7 @@ function updateCredentialsButton() {
// Get the currently selected upstream provider name
function getSelectedUpstreamProvider() {
const providers = (options && options["spot_submit_providers"] && $("#activity").val())
? (options["spot_submit_providers"][$("#activity").val()] || [])
: [];
const providers = SPOT_SUBMIT_PROVIDERS[$("#activity").val()] || [];
if (providers.length === 0) return null;
if (providers.length === 1) return providers[0];
return $("#upstream-provider-select").val();
@@ -302,6 +262,13 @@ $("#upstream-provider-select").change(function () {
// Startup
$(document).ready(function () {
// Load options
loadOptions();
// Load reCAPTCHA if a site key is configured
if (RECAPTCHA_SITE_KEY) {
loadRecaptcha();
}
loadSettings();
// Update the upstream area for any pre-selected activity
updateUpstreamArea();
});
+9 -29
View File
@@ -328,33 +328,6 @@ function addAlertRowsToTable(tbody, alerts) {
});
}
// Load server options. Once a successful callback is made from this, we then query alerts.
function loadOptions() {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
// Populate the filters panel
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#source-options", "source", options["alert_providers"]);
// Alerts can only ever be tagged with activities that have an alert source, so only offer those as filters here
generateActivitiesMultiToggleFilterCard(options["activities"].filter(o => o["alerts_possible"]), false);
// Load URL params. These may select things from the various filter & display options, so the function needs
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
// loading settings, so this needs to be called before that.
loadURLParams();
// Load filters from settings storage
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
// Load alerts and set up the timer
loadAlerts();
setInterval(loadAlerts, REFRESH_INTERVAL_SEC * 1000);
});
}
// Method called when any filter is changed to reload the alerts and persist the filter settings.
function filtersUpdated() {
loadAlerts();
@@ -363,8 +336,15 @@ function filtersUpdated() {
// Startup
$(document).ready(function () {
// Call loadOptions(), this will then trigger loading alerts and setting up timers.
loadOptions();
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
// called first.
loadURLParams();
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
// Load alerts and set up the timer
loadAlerts();
setInterval(loadAlerts, REFRESH_INTERVAL_SEC * 1000);
});
// Reload alerts on becoming visible. This forces a refresh when used as a PWA and the user switches back to the PWA
+14 -42
View File
@@ -108,7 +108,7 @@ function updateBands() {
// Convert to a map of band names to the spots on that band. Bands with no
// spots in view will not be present.
const bandToSpots = new Map();
options["bands"].forEach(function (band) {
BANDS.forEach(function (band) {
const matchingSpots = spotList.filter(function (s) {
return s.band === band.name;
});
@@ -127,7 +127,7 @@ function updateBands() {
table.find('thead tr').append(`<th style='background-color:${bandToColor(spotList[0].band)}; color:${bandToContrastColor(spotList[0].band)}'>${spotList[0].band}</th>`);
// Get the band data to fetch start and end frequencies
let band = options["bands"].filter(function (b) {
let band = BANDS.filter(function (b) {
return b.name === bandName;
})[0];
@@ -278,44 +278,6 @@ function removeDuplicatesForBandPanel(spotList) {
return spotList.filter(s => !spotsToRemove.includes(s.uid));
}
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
// spots repeatedly.
function loadOptions() {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateActivitiesMultiToggleFilterCard(options["activities"]);
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]);
generateSourcesMultiToggleFilterCard(options["spot_providers"], options["spot_providers_enabled_by_default"]);
// Load URL params. These may select things from the various filter & display options, so the function needs
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
// loading settings, so this needs to be called before that.
loadURLParams();
// Load settings from settings storage now all the controls are available
loadSettings();
// Load spots and start SSE for live updates
loadSpots();
// Set up spot expiry checker
setInterval(expireOldSpots, 60 * 1000);
});
}
// Method called when any display property is changed to reload the bands display and persist settings.
function displayUpdated() {
updateBands();
@@ -331,6 +293,16 @@ $(document).ready(function () {
}
});
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
// called first.
loadURLParams();
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
addBandToggleColourCSS();
// Load spots and start SSE for live updates
loadSpots();
// Set up spot expiry checker
setInterval(expireOldSpots, 60 * 1000);
});
+1 -73
View File
@@ -1,5 +1,3 @@
// Storage for the options that the server gives us. This will define our filters.
let options = {};
// Normally load user settings from local storage, unless embedded mode is in use
let useLocalStorage = true;
@@ -124,75 +122,6 @@ function allFilterOptionsSelected(parameter) {
return filter.length === 0;
}
// Generate a filter card with inline checkboxes plus All/None links.
function generateMultiToggleFilterCard(elementID, filterQuery, options) {
const $row = $('<div>');
options.forEach(o => {
$row.append(`<div class="form-check form-check-inline"><input type="checkbox" class="form-check-input filter-button-${filterQuery} storeable-checkbox" id="filter-button-${filterQuery}-${o}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" for="filter-button-${filterQuery}-${o}">${o}</label></div>`);
});
$(elementID).append($row);
$(elementID).append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('${filterQuery}', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('${filterQuery}', false); return false;">None</a></div>`);
}
// Generate a collapsable section for the activities filter card, and append it to the given list. The section is
// expanded by default if isExpanded is true. If hasContent is false (i.e. there are no activities to show in this
// section), the section is omitted entirely rather than showing an empty collapsible heading.
function appendActivityFilterSection($list, sectionId, heading, isExapanded, hasContent, buildBody) {
if (!hasContent) {
return;
}
const collapseId = `activity-options-${sectionId}-collapse`;
const $li = $('<li class="mb-1">');
$li.append(`<button class="btn-toggle${isExapanded ? '' : ' collapsed'}" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="${isExapanded ? 'true' : 'false'}" aria-controls="${collapseId}">${heading}</button>`);
const $body = $(`<div class="collapse filter-section-body${isExapanded ? ' show' : ''}" id="${collapseId}">`);
buildBody($body);
$li.append($body);
$list.append($li);
}
// Build the grid of activity checkboxes for a given (already filtered) list of activities.
function buildActivityFilterGrid(activity_options) {
const $grid = $('<div class="row row-cols-2 g-1 mb-1">');
activity_options.forEach(o => {
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-activity storeable-checkbox" id="filter-button-activity-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-activity-${domSafeName}" for="filter-button-activity-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
});
return $grid;
}
// Generate activities filter card. This one is also a special case. includeGeneralDX controls whether the "General
// DX" (NO_ACTIVITY) option is offered - this doesn't apply on the alerts page, where every alert has an activity.
function generateActivitiesMultiToggleFilterCard(activity_options, includeGeneralDX = true) {
const $list = $('<ul class="list-unstyled filter-section-list ps-0">');
const traditional = activity_options.filter(o => o["activity_type"] === "TRADITIONAL");
appendActivityFilterSection($list, 'traditional', 'Traditional', true, includeGeneralDX || traditional.length > 0, $body => {
if (includeGeneralDX) {
$body.append(`<div class="w-100 mb-1"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-activity storeable-checkbox" id="filter-button-activity-NO_ACTIVITY" value="NO_ACTIVITY" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-activity-NO_ACTIVITY" for="filter-button-activity-NO_ACTIVITY"><i class="fa-solid fa-tower-cell"></i> General DX</label></div></div>`);
}
$body.append(buildActivityFilterGrid(traditional));
});
const adventure = activity_options.filter(o => o["activity_type"] === "ADVENTURE");
appendActivityFilterSection($list, 'adventure', 'Adventure', true, adventure.length > 0, $body => {
$body.append(buildActivityFilterGrid(adventure));
});
const regional = activity_options.filter(o => o["activity_type"] === "REGIONAL");
appendActivityFilterSection($list, 'regional', 'Regional', false, regional.length > 0, $body => {
$body.append(buildActivityFilterGrid(regional));
});
const event = activity_options.filter(o => o["activity_type"] === "EVENT");
appendActivityFilterSection($list, 'event', 'Event', false, event.length > 0, $body => {
$body.append(buildActivityFilterGrid(event));
});
$("#activity-options").append($list);
$("#activity-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('activity', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('activity', false); return false;">None</a></div>`);
}
// Method called when "All" or "None" is clicked
function toggleFilterButtons(filterQuery, state) {
$(".filter-button-" + filterQuery).each(function () {
@@ -238,8 +167,7 @@ function setColorScheme(mode) {
// Startup function to determine whether to use light or dark mode, or leave as auto
function usePreferredTheme() {
// Work out if we have ever explicitly saved the value of our select box. If so, we set our colour scheme now based
// on that. If not, we let the select stay with nothing selected, so that the server sets it to whatever the
// server's default is when the options call is retrieved.
// on that. If not, we leave the colour scheme at the server's default, which is set in the page template.
let val = localStorage.getItem("#color_scheme:value");
if (val != null) {
setColorScheme(JSON.parse(val));
+30 -57
View File
@@ -322,61 +322,6 @@ function getTooltipText(s) {
return ttt;
}
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
// spots repeatedly.
function loadOptions() {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateActivitiesMultiToggleFilterCard(options["activities"]);
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]);
generateSourcesMultiToggleFilterCard(options["spot_providers"], options["spot_providers_enabled_by_default"]);
// Load URL params. These may select things from the various filter & display options, so the function needs
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
// loading settings, so this needs to be called before that.
loadURLParams();
loadMapURLParams();
// Load settings from settings storage now all the controls are available
loadSettings();
// If no basemap has been explicitly saved and the UI is in dark mode, default to dark Mapnik
if (localStorage.getItem("#basemap:value") === null) {
if (document.documentElement.getAttribute("data-bs-theme") === "dark") {
$("#basemap").val("OpenStreetMap.Mapnik.Dark");
}
}
// Apply basemap and overlay settings now that controls have their saved values
setBasemap($("#basemap").val());
setBasemapOpacity(parseFloat($("#basemapOpacity").val()));
enableTerminator($("#showTerminator")[0].checked);
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
enableCQZones($("#showCQZones")[0].checked);
enableITUZones($("#showITUZones")[0].checked);
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
// Load spots and start SSE for live updates
loadSpots();
// Set up spot expiry checker
setInterval(expireOldSpots, 60 * 1000);
});
}
// Method called when any display property is changed to reload the map and persist the display settings.
function displayUpdated() {
updateMap();
@@ -639,8 +584,36 @@ $(document).ready(function () {
$(".hideonmap").hide();
// Set up map
setUpMap();
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
// called first.
loadURLParams();
loadMapURLParams();
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
addBandToggleColourCSS();
// If no basemap has been explicitly saved and the UI is in dark mode, default to dark Mapnik
if (localStorage.getItem("#basemap:value") === null) {
if (document.documentElement.getAttribute("data-bs-theme") === "dark") {
$("#basemap").val("OpenStreetMap.Mapnik.Dark");
}
}
// Apply basemap and overlay settings now that controls have their saved values
setBasemap($("#basemap").val());
setBasemapOpacity(parseFloat($("#basemapOpacity").val()));
enableTerminator($("#showTerminator")[0].checked);
enableMaidenheadGrid($("#showMaidenheadGrid")[0].checked);
enableCQZones($("#showCQZones")[0].checked);
enableITUZones($("#showITUZones")[0].checked);
enableWABWAIGrid($("#showWABWAIGrid")[0].checked);
// Load spots and start SSE for live updates
loadSpots();
// Set up spot expiry checker
setInterval(expireOldSpots, 60 * 1000);
// Display intro box
displayIntroBox();
// Prevent mouse scroll and touch actions in the popup menus being passed through to the map
+21 -48
View File
@@ -461,52 +461,6 @@ function createNewTableRowsForSpot(s, highlightNew) {
return [$tr, $tr2];
}
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
// spots repeatedly.
function loadOptions() {
$.getJSON('/api/v3/options', function (jsonData) {
// Store options
options = jsonData;
// First pass loading settings, so we can load the band colour scheme before the filters that need to use it
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
// Add CSS for band toggle buttons
addBandToggleColourCSS(options["bands"]);
// Populate the filters panel
generateBandsMultiToggleFilterCard(options["bands"]);
generateActivitiesMultiToggleFilterCard(options["activities"]);
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
generateModesMultiToggleFilterCard(options["modes"]);
generateSourcesMultiToggleFilterCard(options["spot_providers"], options["spot_providers_enabled_by_default"]);
// Load URL params. These may select things from the various filter & display options, so the function needs
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
// loading settings, so this needs to be called before that.
loadURLParams();
// Load settings from settings storage now all the controls are available
loadSettings();
// Extra setting - the toggle for the "bearing" & "distance" columns are disabled if the user has not entered a
// valid grid, and normally this logic is handled on user input to the grid field, but we might have just loaded
// a value direct into the field, so apply the same logic here.
$("#tableShowBearing").prop('disabled', !isUserGridValid());
$("#tableShowDistance").prop('disabled', !isUserGridValid());
if (!isUserGridValid()) {
$("#tableShowBearing").prop('checked', false);
$("#tableShowDistance").prop('checked', false);
}
// Load spots (this will also set up the SSE connection to update them too)
loadSpots();
});
}
// Work out if the user's entered grid is a valid Maidenhead grid
function isUserGridValid() {
const userGrid = $("#userGrid").val().toUpperCase();
@@ -572,8 +526,27 @@ function clearWorked() {
// Startup
$(document).ready(function () {
// Call loadOptions(), this will then trigger loading spots and setting up timers.
loadOptions();
// Load URL params. If these ask for "embedded mode", this will suppress loading settings, so this needs to be
// called first.
loadURLParams();
loadSettings();
setColorScheme($("#color_scheme option:selected").val());
setBandColorScheme($("#band_color_scheme option:selected").val());
addBandToggleColourCSS();
// Extra setting - the toggle for the "bearing" & "distance" columns are disabled if the user has not entered a
// valid grid, and normally this logic is handled on user input to the grid field, but we might have just loaded
// a value direct into the field, so apply the same logic here.
$("#tableShowBearing").prop('disabled', !isUserGridValid());
$("#tableShowDistance").prop('disabled', !isUserGridValid());
if (!isUserGridValid()) {
$("#tableShowBearing").prop('checked', false);
$("#tableShowDistance").prop('checked', false);
}
// Load spots (this will also set up the SSE connection to update them too)
loadSpots();
// Display intro box
displayIntroBox();
+7 -49
View File
@@ -7,80 +7,38 @@ let spots = [];
let worked = []
// Dynamically add CSS code for the band checkboxes to show in the appropriate colour.
// Some band names contain decimal points which are not allowed in CSS classes, so we text-replace them to "p".
function addBandToggleColourCSS(band_options) {
function addBandToggleColourCSS() {
const $style = $('<style>');
band_options.forEach(o => {
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$style.append(`#filter-button-label-band-${domSafeName} { padding-left: 0.3em; border-left: 5px solid ${bandToColor(o['name'])};}`);
$(".filter-button-band").each(function () {
$style.append(`label[for="${this.id}"] { padding-left: 0.3em; border-left: 5px solid ${bandToColor(this.value)};}`);
});
$('html > head').append($style);
}
// Generate bands filter card. This one is a special case.
function generateBandsMultiToggleFilterCard(band_options) {
const $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 row-cols-xxl-4 g-1 mb-1">');
band_options.forEach(o => {
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-band storeable-checkbox" id="filter-button-band-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked> <label class="form-check-label" id="filter-button-label-band-${domSafeName}" for="filter-button-band-${domSafeName}">${o['name']}</label></div></div>`);
});
$("#band-options").append($grid);
$("#band-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('band', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('band', false); return false;">None</a> &nbsp; <a href="#" onclick="setHamHFBandToggles(); return false;">Ham HF only</a></div>`);
}
// Set the band toggles so that only the amateur radio HF bands are selected. This includes 160m and 6m because that's
// widely expected by hams to be included. Special case of toggleFilterButtons().
// Set the band toggles so that only the amateur radio HF bands are selected. Special case of toggleFilterButtons().
function setHamHFBandToggles() {
const hamHFBands = ["160m", "80m", "60m", "40m", "30m", "20m", "17m", "15m", "12m", "10m", "6m"];
$(".filter-button-band").each(function () {
$(this).prop('checked', hamHFBands.includes($(this).val().replace("filter-button-band-", "")));
$(this).prop('checked', $(this).data("ham-hf"));
});
filtersUpdated();
}
// Generate modes filter card. This one is also a special case.
function generateModesMultiToggleFilterCard(mode_options) {
const $grid = $('<div class="row row-cols-3 row-cols-md-2 row-cols-lg-3 g-1 mb-1">');
mode_options.forEach(o => {
const domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-mode storeable-checkbox" id="filter-button-mode-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-mode-${domSafeName}" for="filter-button-mode-${domSafeName}">${o}</label></div></div>`);
});
$("#mode-options").append($grid);
$("#mode-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('mode', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('mode', false); return false;">None</a> &nbsp; <a href="#" onclick="setVoiceModeToggles(); return false;">Voice only</a> &nbsp; <a href="#" onclick="setDigiModeToggles(); return false;">Digimodes only</a></div>`);
}
// Set the mode toggles that relate to Analog Voice.
function setVoiceModeToggles() {
const modes = ["PHONE", "SSB", "AM", "FM", "DV"];
$(".filter-button-mode").each(function () {
$(this).prop('checked', modes.includes($(this).val().replace("filter-button-mode-", "")));
$(this).prop('checked', $(this).data("mode-type") === "PHONE");
});
filtersUpdated();
}
// Set the mode toggles that relate to Digimodes.
function setDigiModeToggles() {
const modes = ["DATA", "FT8", "FT4", "RTTY", "SSTV", "JS8", "HELL", "PSK", "OLIVIA", "PKT", "MSK144"];
$(".filter-button-mode").each(function () {
$(this).prop('checked', modes.includes($(this).val().replace("filter-button-mode-", "")));
$(this).prop('checked', $(this).data("mode-type") === "DATA");
});
filtersUpdated();
}
// Generate Sources filter card. This one is a minor special case as we create the checkboxes in the normal way, but
// set which ones are enabled by default based on config rather than having them all enabled by default. We also sanitise
// names here for HTML elements.
function generateSourcesMultiToggleFilterCard(source_options, sources_enabled_by_default) {
const $grid = $('<div class="row row-cols-2 row-cols-xxl-3 g-1 mb-1">');
source_options.forEach(o => {
const enable = sources_enabled_by_default.includes(o);
const domSafeName = o.replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-source storeable-checkbox" id="filter-button-source-${domSafeName}" value="${o}" autocomplete="off" onClick="filtersUpdated()" ${enable ? "checked" : ""}><label class="form-check-label" for="filter-button-source-${domSafeName}">${o}</label></div></div>`);
});
$("#source-options").append($grid);
$("#source-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('source', true); return false;">All</a> &nbsp; <a href="#" onclick="toggleFilterButtons('source', false); return false;">None</a></div>`);
}
// Method called when any filter is changed to reload the spots and persist the filter settings.
function filtersUpdated() {
loadSpots();