1
0
Fork 0
timelinize/frontend/resources/js/settings.js
2025-04-21 21:49:41 -06:00

249 lines
No EOL
8.5 KiB
JavaScript

// Changes the settings tab that is active, and settings page content.
// The target should include the hash (#) prefix and should correspond
// with the href and div IDs.
function changeSettingsTab(target) {
$$(`.settings-nav .active`).forEach(elem => {
elem.classList.remove('active');
});
$(`.settings-nav a.list-group-item[href="${target}"]`).classList.add('active');
$$(`.settings-page:not(${target}, .d-none)`).forEach(elem => {
elem.classList.add('d-none');
});
$(target).classList.remove('d-none');
document.title = `Settings - ${$('h2', target).textContent}`;
// FIXME: For some reason this causes a duplicate history entry to be added, but after navigating again it disappears
window.history.pushState(null, null, target);
}
// when the map is moved into the location picker, set up its interactive draw features;
// and when it is removed from the location picker, reset its configuration
document.addEventListener('mapMoved', async e => {
if (e.detail?.currentElement?.matches('#secret-location-picker .map-container'))
{
// map inserted
const modes = MapboxDrawGeodesic.enable(MapboxDraw.modes);
const draw = new MapboxDraw({
displayControlsDefault: false,
controls: {
trash: true
},
modes
});
tlz.mapDrawBar = new extendDrawBar({
draw: draw,
buttons: [
{
on: 'click',
action: function() {
draw.changeMode('draw_circle');
},
classes: ['text-black', 'add-circle'],
html: `
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="icon icon-tabler icons-tabler-outline icon-tabler-circle-plus-2">
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M20.985 12.522a9 9 0 1 0 -8.475 8.464" />
<path d="M16 19h6" />
<path d="M19 16v6" />
</svg>
`
}
]
});
// When a circle is created, add its corresponding element to the list
tlz.map.on('draw.create', settingsMapLocObfuscationDrawCreate);
// When a circle is changed, update its corresponding element in the list
tlz.map.on('draw.update', settingsMapLocObfuscationUpdate);
// When a circle is deleted, remove its corresponding element from the list
tlz.map.on('draw.delete', settingsMapLocObfuscationDelete);
// When a circle is selected, highlight its corresponding element in the list
tlz.map.on('draw.selectionchange', settingsMapLocObfuscationSelChange);
// addControl takes an optional second argument to set the position of the control.
// If no position is specified the control defaults to `top-right`. See the docs
// for more details: https://docs.mapbox.com/mapbox-gl-js/api/#map#addcontrol
tlz.map.addControl(tlz.mapDrawBar);
if (tlz.settings?.application?.obfuscation?.locations) {
for (const loc of tlz.settings.application.obfuscation.locations) {
const circle = MapboxDrawGeodesic.createCircle([loc.lon, loc.lat], loc.radius_meters/1000);
circle.properties.name = loc.description;
const featureIDs = draw.add(circle);
settingsMapLocObfuscationDrawCreate({features: [circle]});
}
}
}
// when the element is no longer in the DOM, we can't use descendency selectors to match it
else if (e.detail?.previousElement?.matches('.secret-location-picker.map-container'))
{
// map removed
if (tlz.mapDrawBar) {
tlz.map.removeControl(tlz.mapDrawBar);
delete(tlz.mapDrawBar);
tlz.map.off('draw.create', settingsMapLocObfuscationDrawCreate);
tlz.map.off('draw.update', settingsMapLocObfuscationUpdate);
tlz.map.off('draw.delete', settingsMapLocObfuscationDelete);
tlz.map.off('draw.selectionchange', settingsMapLocObfuscationSelChange);
}
}
});
function settingsMapLocObfuscationDrawCreate(e) {
const geojson = e.features[0];
const elem = cloneTemplate('#tpl-secret-location');
elem.id = "secret-location-"+geojson.id;
if (MapboxDrawGeodesic.isCircle(geojson)) {
elem._center = MapboxDrawGeodesic.getCircleCenter(geojson); // [lon, lat]
elem._radius = MapboxDrawGeodesic.getCircleRadius(geojson); // kilometers
$('.secret-location-coords', elem).innerText = `${elem._center[1].toFixed(4)}, ${elem._center[0].toFixed(4)}`;
$('.secret-location-radius', elem).innerText = `${elem._radius.toFixed(2)} km`;
$('.secret-location-name', elem).value = geojson.properties.name || "";
}
$('#secret-location-list').append(elem);
}
function settingsMapLocObfuscationUpdate(e) {
const geojson = e.features[0];
// this event fires if a circle point (the center or on the circumference, like while editing
// the feature) is selected when the feature is deleted, but the coordinates are empty; avoid crashing
if (MapboxDrawGeodesic.isCircle(geojson) && geojson.geometry.coordinates.length > 0) {
const elem = $('#secret-location-'+geojson.id);
elem._center = MapboxDrawGeodesic.getCircleCenter(geojson); // [lon, lat]
elem._radius = MapboxDrawGeodesic.getCircleRadius(geojson); // kilometers
$('.secret-location-coords', elem).innerText = `${elem._center[1].toFixed(4)}, ${elem._center[0].toFixed(4)}`;
$('.secret-location-radius', elem).innerText = `${elem._radius.toFixed(2)} km`;
}
}
function settingsMapLocObfuscationDelete(e) {
const geojson = e.features[0];
if (MapboxDrawGeodesic.isCircle(geojson)) {
$('#secret-location-'+geojson.id).remove();
}
}
function settingsMapLocObfuscationSelChange(e) {
$$('.secret-location.selected-location').forEach(elem => {
elem.classList.remove('selected-location');
});
e.features.forEach(feature => {
$('#secret-location-'+feature.id).classList.add('selected-location');
});
}
// allows us to extend the control bar for Mapbox-gl-draw with our own buttons.
class extendDrawBar {
constructor(opt) {
let ctrl = this;
ctrl.draw = opt.draw;
ctrl.buttons = opt.buttons || [];
ctrl.onAddOrig = opt.draw.onAdd;
ctrl.onRemoveOrig = opt.draw.onRemove;
}
onAdd(map) {
let ctrl = this;
ctrl.map = map;
ctrl.elContainer = ctrl.onAddOrig(map);
ctrl.buttons.forEach((b) => {
ctrl.addButton(b);
});
return ctrl.elContainer;
}
onRemove(map) {
let ctrl = this;
ctrl.buttons.forEach((b) => {
ctrl.removeButton(b);
});
ctrl.onRemoveOrig(map);
}
addButton(opt) {
let ctrl = this;
var elButton = document.createElement('button');
elButton.className = 'mapbox-gl-draw_ctrl-draw-btn';
if (opt.classes instanceof Array) {
opt.classes.forEach((c) => {
elButton.classList.add(c);
});
}
elButton.addEventListener(opt.on, opt.action);
elButton.innerHTML = opt.html || "";
ctrl.elContainer.prepend(elButton);
opt.elButton = elButton;
}
removeButton(opt) {
opt.elButton.removeEventListener(opt.on, opt.action);
opt.elButton.remove();
}
}
// change tabs
on('click', '.settings-nav a', e => {
// prevent popstate (yes, really: apparently since pushState is used elsewhere)
// from triggering and invoking navigateSPA -- I don't fully understand this;
// I guess popstate is the "default" action when a link is clicked in that case
e.preventDefault();
// get the href literally, not expanded as e.target.href is
changeSettingsTab(e.target.getAttribute('href'));
return false;
});
// save settings!
on('click', '#submit-settings', async event => {
const mutatedSettings = {
application: {
"app.obfuscation.enabled": $('#demo-mode-enabled').checked,
"app.obfuscation.data_files": $('#data-file-names').checked,
"app.mapbox_api_key": $('#mapbox-api-key').value,
"app.website_dir": $('#website-dir').value
}
};
// only add the locations key to the object if locations were rendered, which only
// happens if the location obfuscation map comes into view; otherwise we could
// wipe out all configured locations if we submit this value as empty or null!
if (tlz.mapDrawBar) {
// build array of obfuscated locations
const locations = [];
$$('.secret-location').forEach(el => {
locations.push({
description: $('.secret-location-name', el).value,
lat: el._center[1],
lon: el._center[0],
radius_meters: Math.round(el._radius*1000) // must be an integer for compatibility with the integer type in the Go backend
});
});
mutatedSettings.application["app.obfuscation.locations"] = locations;
}
// when saving settings, the new settings are returned, so update our copy
tlz.settings = await app.ChangeSettings(mutatedSettings);
notify({
type: "success",
title: "Settings saved",
duration: 2000
});
// if demo mode setting changed, reload relevant page elements
if ($('#demo-mode-enabled').checked != $('#demo-mode-enabled').dataset.originalValue) {
updateRepoOwners(true);
}
});