Fix note UX: unreachable wiki link, white Save hover, stale icon preview, invisible new points
- "Nearby on Wikipedia" linked to KIWIX_URL, a Docker-internal hostname (http://kiwix:8080) no real browser can resolve. Split it: KIWIX_URL stays server-side-only for the reachability check, and the link itself is built from KIWIX_PUBLIC_PORT (kiwix-serve's newly published host port) plus whatever host the visitor already used to reach maptool — works regardless of which hostname/IP that is. - The Save button went white on hover in GTK-themed Chrome: the browser's native button skin was painting over the custom background because appearance wasn't reset. - The map preview marker for a pending note always showed a plain red pin no matter which icon was picked in the form. NoteSaveForm now reports icon changes via onIconChange so NotesControl can update the marker's glyph live. - Saving a note (or a Measure "Save as note") left the new/grown .geojson file unchecked and invisible until manually toggled on. notifyGpxStoreChanged now carries the affected path so GpxPanelControl auto-shows it.
This commit is contained in:
1 parent
4f022ec43a
commit
9997a8a8c0
7 files changed
+103
-17
No files matched your search
@@ -73,9 +73,21 @@ module Maptool
|
|||||||
# An external kiwix-serve instance (see docs/development.adoc), not
|
# An external kiwix-serve instance (see docs/development.adoc), not
|
||||||
# something this app runs itself — blank disables the "Nearby on
|
# something this app runs itself — blank disables the "Nearby on
|
||||||
# Wikipedia" popup link the same way the other optional-integration
|
# Wikipedia" popup link the same way the other optional-integration
|
||||||
# blanks above do.
|
# blanks above do. This is reached server-side only (the reachability
|
||||||
|
# check below, same-Docker-network service name in production, e.g.
|
||||||
|
# "http://kiwix:8080") — it is NOT what gets handed to the browser, see
|
||||||
|
# KIWIX_PUBLIC_PORT.
|
||||||
KIWIX_URL = ENV.fetch("KIWIX_URL", "")
|
KIWIX_URL = ENV.fetch("KIWIX_URL", "")
|
||||||
|
|
||||||
|
# The port kiwix-serve is published on for browsers to reach directly
|
||||||
|
# (docker-compose.yaml's kiwix service ports: mapping) — KIWIX_URL above
|
||||||
|
# is a Docker-network-internal hostname a real browser can't resolve at
|
||||||
|
# all, so the "Nearby on Wikipedia" link is instead built from whatever
|
||||||
|
# host the visitor already used to reach maptool itself
|
||||||
|
# (request.host) plus this port. Blank disables the link, same as a
|
||||||
|
# blank KIWIX_URL.
|
||||||
|
KIWIX_PUBLIC_PORT = ENV.fetch("KIWIX_PUBLIC_PORT", "")
|
||||||
|
|
||||||
# Login delegates to the Gitea instance the user already has an account
|
# Login delegates to the Gitea instance the user already has an account
|
||||||
# on, rather than maintaining a local username/password — see
|
# on, rather than maintaining a local username/password — see
|
||||||
# docs/architecture.adoc. Only users listed in ALLOWED_GITEA_USERS may
|
# docs/architecture.adoc. Only users listed in ALLOWED_GITEA_USERS may
|
||||||
@@ -208,9 +220,14 @@ module Maptool
|
|||||||
|
|
||||||
# Pinged once per page load (public/js/map.js) — url is handed back so
|
# Pinged once per page load (public/js/map.js) — url is handed back so
|
||||||
# the frontend can build the geo-search link itself; this server never
|
# the frontend can build the geo-search link itself; this server never
|
||||||
# proxies to kiwix-serve, just says whether it's there.
|
# proxies to kiwix-serve, just says whether it's there. url is built
|
||||||
|
# from request.host (whatever hostname/IP the visitor already used to
|
||||||
|
# reach maptool itself) + KIWIX_PUBLIC_PORT, not KIWIX_URL — that's a
|
||||||
|
# Docker-network-internal address only this server can resolve.
|
||||||
get "/api/kiwix" do
|
get "/api/kiwix" do
|
||||||
json available: Kiwix.reachable?(KIWIX_URL), url: KIWIX_URL
|
reachable = Kiwix.reachable?(KIWIX_URL)
|
||||||
|
url = KIWIX_PUBLIC_PORT.empty? ? "" : "http://#{request.host}:#{KIWIX_PUBLIC_PORT}"
|
||||||
|
json available: reachable && !url.empty?, url: url
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/api/gpx/tree" do
|
get "/api/gpx/tree" do
|
||||||
|
|||||||
@@ -37,7 +37,14 @@ services:
|
|||||||
# actually being present: GET /api/kiwix pings it and just reports
|
# actually being present: GET /api/kiwix pings it and just reports
|
||||||
# unavailable if the container isn't up/reachable, same story as
|
# unavailable if the container isn't up/reachable, same story as
|
||||||
# every other optional-local-file feature (offline.pmtiles, etc).
|
# every other optional-local-file feature (offline.pmtiles, etc).
|
||||||
|
# Server-side reachability check only — a browser on the LAN can't
|
||||||
|
# resolve this hostname, see KIWIX_PUBLIC_PORT below.
|
||||||
KIWIX_URL: "http://kiwix:8080"
|
KIWIX_URL: "http://kiwix:8080"
|
||||||
|
# The host port kiwix's own ports: mapping below publishes it on —
|
||||||
|
# app.rb builds the browser-facing "Nearby on Wikipedia" link from
|
||||||
|
# whatever host the visitor used to reach maptool itself plus this
|
||||||
|
# port, since KIWIX_URL above is Docker-internal only.
|
||||||
|
KIWIX_PUBLIC_PORT: "8090"
|
||||||
volumes:
|
volumes:
|
||||||
# Uploaded GPX files and the offline PMTiles extract — kept outside
|
# Uploaded GPX files and the offline PMTiles extract — kept outside
|
||||||
# the image so a rebuild/redeploy doesn't wipe them (see
|
# the image so a rebuild/redeploy doesn't wipe them (see
|
||||||
@@ -87,5 +94,10 @@ services:
|
|||||||
# passing it again produced "Unexpected argument: --port=8080,
|
# passing it again produced "Unexpected argument: --port=8080,
|
||||||
# --port=8080, /data/wikipedia.zim" and kiwix-serve refused to start.
|
# --port=8080, /data/wikipedia.zim" and kiwix-serve refused to start.
|
||||||
command: ["/data/wikipedia.zim"]
|
command: ["/data/wikipedia.zim"]
|
||||||
|
# Published so browsers on the LAN can reach kiwix-serve's own search
|
||||||
|
# UI directly (see maptool's KIWIX_PUBLIC_PORT above) — 8090, not 8080,
|
||||||
|
# since 8080 is already taken by another container on this host.
|
||||||
|
ports:
|
||||||
|
- "8090:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/kiwix/wikipedia.zim:/data/wikipedia.zim
|
- ./data/kiwix/wikipedia.zim:/data/wikipedia.zim
|
||||||
+10
-5
@@ -77,11 +77,16 @@ outline rather than a single point).
|
|||||||
A point popup's "Nearby on Wikipedia" link needs
|
A point popup's "Nearby on Wikipedia" link needs
|
||||||
https://github.com/kiwix/kiwix-tools[kiwix-serve] running with a
|
https://github.com/kiwix/kiwix-tools[kiwix-serve] running with a
|
||||||
Wikipedia https://library.kiwix.org[ZIM] loaded — set `KIWIX_URL` to
|
Wikipedia https://library.kiwix.org[ZIM] loaded — set `KIWIX_URL` to
|
||||||
wherever it's reachable (`docker-compose.yaml` already runs a `kiwix`
|
wherever *the server* can reach it (`docker-compose.yaml` already runs
|
||||||
service pointed at `data/kiwix/wikipedia.zim` and wires `KIWIX_URL` to
|
a `kiwix` service pointed at `data/kiwix/wikipedia.zim` and wires
|
||||||
it; blank/unset disables the link, same as every other optional
|
`KIWIX_URL` to it via its compose service name; blank/unset disables
|
||||||
integration here). Not part of the repo — a ZIM is a large,
|
the link, same as every other optional integration here). This is
|
||||||
self-contained archive, obtain one locally:
|
used only for the server-side reachability check — a browser can't
|
||||||
|
resolve a Docker-internal hostname like `kiwix`, so the link itself is
|
||||||
|
built from `KIWIX_PUBLIC_PORT` (the host port kiwix-serve is
|
||||||
|
`ports:`-published on, `8090` in `docker-compose.yaml`) plus whatever
|
||||||
|
host the visitor already used to reach maptool itself. Not part of the
|
||||||
|
repo — a ZIM is a large, self-contained archive, obtain one locally:
|
||||||
|
|
||||||
* https://library.kiwix.org/#lang=eng&category=wikipedia[Kiwix's own
|
* https://library.kiwix.org/#lang=eng&category=wikipedia[Kiwix's own
|
||||||
library] has pre-built Wikipedia ZIMs — a `_maxi` variant per
|
library] has pre-built Wikipedia ZIMs — a `_maxi` variant per
|
||||||
|
|||||||
@@ -930,6 +930,29 @@ html, body {
|
|||||||
background: #dbe9ff;
|
background: #dbe9ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The pending-note marker shown on the map while the save form is open —
|
||||||
|
updates live to whatever icon is picked (see notes.js's onIconChange). A
|
||||||
|
plain round badge (not a drop-pin shape) since MapLibre anchors a custom
|
||||||
|
marker element at its center, and a pin's usual tip-points-at-the-spot
|
||||||
|
shape needs a bottom anchor to line up correctly. */
|
||||||
|
.notes-panel__preview-marker {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #c0392b;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-panel__preview-marker svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
.notes-panel__target,
|
.notes-panel__target,
|
||||||
.notes-panel__new-name {
|
.notes-panel__new-name {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -948,6 +971,11 @@ html, body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notes-panel__actions button {
|
.notes-panel__actions button {
|
||||||
|
/* Without this, some browsers (GTK-themed Chrome on Linux in
|
||||||
|
particular) paint their own native hover/active highlight over a
|
||||||
|
button's background instead of leaving a custom one alone — the
|
||||||
|
Save button visibly went white on hover because of exactly this. */
|
||||||
|
appearance: none;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
|
|||||||
+11
-2
@@ -60,8 +60,17 @@ export class GpxPanelControl {
|
|||||||
|
|
||||||
// notes.js writes directly into the same GPX store (a hand-drawn note
|
// notes.js writes directly into the same GPX store (a hand-drawn note
|
||||||
// saved into a .geojson layer) — refresh so a newly created/grown file
|
// saved into a .geojson layer) — refresh so a newly created/grown file
|
||||||
// shows up in the tree without a manual reopen.
|
// shows up in the tree without a manual reopen, and auto-show it (check
|
||||||
onGpxStoreChanged(() => this._loadTree());
|
// + render) since a note the user just placed being invisible until
|
||||||
|
// they find and check its layer by hand defeats the point of dropping
|
||||||
|
// a pin right there on the map.
|
||||||
|
onGpxStoreChanged((path) => {
|
||||||
|
if (path) {
|
||||||
|
this._visible.add(path);
|
||||||
|
this._reloadFile(path);
|
||||||
|
}
|
||||||
|
this._loadTree();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Registered once, always white, as plain (non-SDF) images: these used to
|
// Registered once, always white, as plain (non-SDF) images: these used to
|
||||||
|
|||||||
+15
-3
@@ -45,9 +45,10 @@ export function generateNoteId() {
|
|||||||
// container it's given (each caller owns where that sits in its own panel);
|
// container it's given (each caller owns where that sits in its own panel);
|
||||||
// hidden until open() is called.
|
// hidden until open() is called.
|
||||||
export class NoteSaveForm {
|
export class NoteSaveForm {
|
||||||
constructor(container, { onSaved, onCancel } = {}) {
|
constructor(container, { onSaved, onCancel, onIconChange } = {}) {
|
||||||
this._onSaved = onSaved;
|
this._onSaved = onSaved;
|
||||||
this._onCancel = onCancel;
|
this._onCancel = onCancel;
|
||||||
|
this._onIconChange = onIconChange;
|
||||||
|
|
||||||
this._el = document.createElement("div");
|
this._el = document.createElement("div");
|
||||||
this._el.className = "notes-panel__form";
|
this._el.className = "notes-panel__form";
|
||||||
@@ -85,6 +86,7 @@ export class NoteSaveForm {
|
|||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
this._selectedIcon = button.dataset.icon;
|
this._selectedIcon = button.dataset.icon;
|
||||||
this._iconPicker.querySelectorAll(".notes-panel__icon-option").forEach((b) => b.classList.toggle("active", b === button));
|
this._iconPicker.querySelectorAll(".notes-panel__icon-option").forEach((b) => b.classList.toggle("active", b === button));
|
||||||
|
this._onIconChange?.(this._selectedIcon);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this._target.addEventListener("change", () => {
|
this._target.addEventListener("change", () => {
|
||||||
@@ -106,6 +108,7 @@ export class NoteSaveForm {
|
|||||||
this._body.value = "";
|
this._body.value = "";
|
||||||
this._selectedIcon = "";
|
this._selectedIcon = "";
|
||||||
this._iconPicker.querySelectorAll(".notes-panel__icon-option").forEach((b, i) => b.classList.toggle("active", i === 0));
|
this._iconPicker.querySelectorAll(".notes-panel__icon-option").forEach((b, i) => b.classList.toggle("active", i === 0));
|
||||||
|
this._onIconChange?.(this._selectedIcon);
|
||||||
// An icon only makes sense on a single point — Measure's "Save as
|
// An icon only makes sense on a single point — Measure's "Save as
|
||||||
// note" can hand this a LineString/Polygon, which has no one spot for
|
// note" can hand this a LineString/Polygon, which has no one spot for
|
||||||
// a badge glyph to sit at.
|
// a badge glyph to sit at.
|
||||||
@@ -173,7 +176,8 @@ export class NoteSaveForm {
|
|||||||
try {
|
try {
|
||||||
const response = await fetch("/api/gpx/notes", { method: "POST", body: requestBody });
|
const response = await fetch("/api/gpx/notes", { method: "POST", body: requestBody });
|
||||||
if (!response.ok) throw new Error(`save failed: ${response.status}`);
|
if (!response.ok) throw new Error(`save failed: ${response.status}`);
|
||||||
notifyGpxStoreChanged();
|
const { path } = await response.json();
|
||||||
|
notifyGpxStoreChanged(path);
|
||||||
this.close();
|
this.close();
|
||||||
this._onSaved?.();
|
this._onSaved?.();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -219,6 +223,11 @@ export class NotesControl {
|
|||||||
this._hint.hidden = false;
|
this._hint.hidden = false;
|
||||||
this._removeMarker();
|
this._removeMarker();
|
||||||
},
|
},
|
||||||
|
// Keeps the map preview in sync with the icon picker — it used to
|
||||||
|
// always show a plain red pin no matter which icon got picked.
|
||||||
|
onIconChange: (icon) => {
|
||||||
|
if (this._marker) this._marker.getElement().innerHTML = icon && POI_ICONS[icon] ? POI_ICONS[icon] : NOTE_PIN_ICON;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this._button.addEventListener("click", () => this._setActive(!this._active));
|
this._button.addEventListener("click", () => this._setActive(!this._active));
|
||||||
@@ -226,7 +235,10 @@ export class NotesControl {
|
|||||||
this._onClick = (event) => {
|
this._onClick = (event) => {
|
||||||
if (!this._active || this._form.isOpen) return;
|
if (!this._active || this._form.isOpen) return;
|
||||||
this._hint.hidden = true;
|
this._hint.hidden = true;
|
||||||
this._marker = new Marker({ color: "#c0392b" }).setLngLat(event.lngLat).addTo(this._map);
|
const el = document.createElement("div");
|
||||||
|
el.className = "notes-panel__preview-marker";
|
||||||
|
el.innerHTML = NOTE_PIN_ICON;
|
||||||
|
this._marker = new Marker({ element: el }).setLngLat(event.lngLat).addTo(this._map);
|
||||||
this._form.open({ type: "Point", coordinates: [event.lngLat.lng, event.lngLat.lat] });
|
this._form.open({ type: "Point", coordinates: [event.lngLat.lng, event.lngLat.lat] });
|
||||||
};
|
};
|
||||||
map.on("click", this._onClick);
|
map.on("click", this._onClick);
|
||||||
|
|||||||
+7
-4
@@ -18,13 +18,16 @@ export function onlyOneSidePanelOpen(panel, onClosedByOther) {
|
|||||||
|
|
||||||
// Lets a control that writes into the GPX store (e.g. notes.js saving a
|
// Lets a control that writes into the GPX store (e.g. notes.js saving a
|
||||||
// drawn point) tell GpxPanelControl its tree is stale, without either one
|
// drawn point) tell GpxPanelControl its tree is stale, without either one
|
||||||
// holding a reference to the other.
|
// holding a reference to the other. path (the .geojson file just written
|
||||||
|
// to) lets the listener also auto-show that file instead of just refreshing
|
||||||
|
// the tree — the point/shape just saved is otherwise invisible until the
|
||||||
|
// user finds and checks it by hand.
|
||||||
const STORE_CHANGED_EVENT = "gpx-store-changed";
|
const STORE_CHANGED_EVENT = "gpx-store-changed";
|
||||||
|
|
||||||
export function notifyGpxStoreChanged() {
|
export function notifyGpxStoreChanged(path) {
|
||||||
document.dispatchEvent(new CustomEvent(STORE_CHANGED_EVENT));
|
document.dispatchEvent(new CustomEvent(STORE_CHANGED_EVENT, { detail: path }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onGpxStoreChanged(callback) {
|
export function onGpxStoreChanged(callback) {
|
||||||
document.addEventListener(STORE_CHANGED_EVENT, callback);
|
document.addEventListener(STORE_CHANGED_EVENT, (event) => callback(event.detail));
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user