Note UX polish: pin marker, icon picker, fix Save button color
- The clicked location now stays marked (a plain maplibre-gl Marker, removed on save/cancel/deactivate) while the note form is open — before, nothing on the map showed where the pending note would land. - POI_ICONS gains a dozen more symbols (water, dump station, toilets, playground, viewpoint, swimming, fishing, climbing, hiking, …) via Object.assign after PARK4NIGHT_TAGS' own icon constants are declared (they're below POI_ICONS in the file, so the literal itself can't reference them directly) — reused as real marker-badge glyphs, not just PARK4NIGHT_TAGS' inline description-tag icons. NoteSaveForm gets a picker grid for these, shown only for a Point note (a Measure-drawn line/polygon has no one spot for a badge). - Fixed: .notes-panel__save's blue background was losing to .notes-panel__actions button's gray — same class-vs-class+element specificity trap .gpx-panel__action[hidden] already has a comment about elsewhere in this file. The button was never actually blue. Verified live (properly simulating a logged-in session this time, via Page.addScriptToEvaluateOnNewDocument rather than a post-load DOM poke, since NotesControl's mount decision happens once, synchronously, at page load): marker appears and clears correctly, 19 icons + "none" render and select, Save renders #1a5fb4. Full suite: 115 runs, 0 failures.
This commit is contained in:
1 parent
465a644365
commit
5576d2edf1
4 files changed
+142
-5
No files matched your search
@@ -47,6 +47,9 @@ independence from connectivity.
|
|||||||
** [x] Fixed: a note saved before `properties.id` existed (the earliest test notes) had no Edit/Delete-able target — the popup no longer offers them without an id, and `rake gpx:backfill_note_ids` assigns one to any pre-existing Note feature missing it
|
** [x] Fixed: a note saved before `properties.id` existed (the earliest test notes) had no Edit/Delete-able target — the popup no longer offers them without an id, and `rake gpx:backfill_note_ids` assigns one to any pre-existing Note feature missing it
|
||||||
** [x] Title + body split (name in the black header, body in a separate field) with light self-authored markdown (`**bold**`, `*italic*`, auto-linked URLs) in the body — a *new*, separate `formatNoteBody` (not `formatDescription`, which stays plain/untrusted-safe for uploaded GPX/park4night content per docs/nps-resources.adoc's earlier markdown-rendering call)
|
** [x] Title + body split (name in the black header, body in a separate field) with light self-authored markdown (`**bold**`, `*italic*`, auto-linked URLs) in the body — a *new*, separate `formatNoteBody` (not `formatDescription`, which stays plain/untrusted-safe for uploaded GPX/park4night content per docs/nps-resources.adoc's earlier markdown-rendering call)
|
||||||
** [x] Gated behind login: NotesControl isn't added to the map at all when logged out, Measure's "Save as note" button is hidden (Measure itself stays usable either way)
|
** [x] Gated behind login: NotesControl isn't added to the map at all when logged out, Measure's "Save as note" button is hidden (Measure itself stays usable either way)
|
||||||
|
** [x] The clicked point stays marked on the map while the note form is open (a plain `Marker`, removed on save/cancel/deactivate) — it was disappearing back into the flat map with nothing showing where the note would land
|
||||||
|
** [x] Icon picker for point notes (a grid in the save form) — extends `POI_ICONS` with more of the symbols `PARK4NIGHT_TAGS` already vendors (water, dump station, toilets, playground, viewpoint, swimming, fishing, climbing, hiking, …), reusable as real marker-badge glyphs, not just inline tag icons. Hidden for a Measure-drawn line/polygon note — no single spot for a badge to sit at
|
||||||
|
** [x] Fixed: the Save button's own blue background was losing to `.notes-panel__actions button`'s gray — same class-vs-class+element specificity trap as `.gpx-panel__action[hidden]` already has a comment about elsewhere in this file, just not yet applied here
|
||||||
** [ ] The Unigrid Matrix category filter only re-slices the *points* source (`_applyCategoryFilter`) — toggling the "Note" category off hides point notes but not polygon/line ones sharing that category
|
** [ ] The Unigrid Matrix category filter only re-slices the *points* source (`_applyCategoryFilter`) — toggling the "Note" category off hides point notes but not polygon/line ones sharing that category
|
||||||
* [ ] Offline satellite tile caching (e.g. ESRI) for a selected route area — key for judging surface quality and obstacles when wild camping
|
* [ ] Offline satellite tile caching (e.g. ESRI) for a selected route area — key for judging surface quality and obstacles when wild camping
|
||||||
|
|
||||||
|
|||||||
+47
-1
@@ -888,6 +888,48 @@ html, body {
|
|||||||
resize: vertical;
|
resize: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notes-panel__icons {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(6, 1fr);
|
||||||
|
gap: 2px;
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Same "author display wins over the UA [hidden] rule" gotcha as
|
||||||
|
.gpx-panel__action[hidden] elsewhere in this file. */
|
||||||
|
.notes-panel__icons[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-panel__icon-option {
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 3px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: none;
|
||||||
|
color: #555;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-panel__icon-option svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-panel__icon-option:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes-panel__icon-option.active {
|
||||||
|
border-color: #1a5fb4;
|
||||||
|
color: #1a5fb4;
|
||||||
|
background: #dbe9ff;
|
||||||
|
}
|
||||||
|
|
||||||
.notes-panel__target,
|
.notes-panel__target,
|
||||||
.notes-panel__new-name {
|
.notes-panel__new-name {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -917,7 +959,11 @@ html, body {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notes-panel__save {
|
/* ".notes-panel__actions " prefix (not just ".notes-panel__save") to
|
||||||
|
outrank ".notes-panel__actions button" — same class-count-vs-class+
|
||||||
|
element specificity trap as elsewhere in this file; without it this
|
||||||
|
lost and the Save button just looked like a second Cancel button. */
|
||||||
|
.notes-panel__actions .notes-panel__save {
|
||||||
background: #1a5fb4;
|
background: #1a5fb4;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -305,6 +305,29 @@ export const PARK4NIGHT_TAGS = {
|
|||||||
windsurf: { icon: WIND_SURFING_ICON, label: "Windsurfing" },
|
windsurf: { icon: WIND_SURFING_ICON, label: "Windsurfing" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extends POI_ICONS (and so POI_ICON_KEYS, and what a marker badge can
|
||||||
|
// show — see gpx.js's _registerCategoryGlyphs) with a handful of the same
|
||||||
|
// symbols PARK4NIGHT_TAGS above already vendors, reused rather than
|
||||||
|
// duplicated — this app's own hand-drawn notes (notes.js's icon picker)
|
||||||
|
// need marker-badge glyphs for "water source"/"dump station"/"viewpoint"
|
||||||
|
// far more than the original OSM-POI-upload set (parking/hotel/fuel) ever
|
||||||
|
// covered. Object.assign, not more entries in the POI_ICONS literal
|
||||||
|
// itself, since these constants are declared below it in this file.
|
||||||
|
Object.assign(POI_ICONS, {
|
||||||
|
amenity_drinking_water: DRINKING_WATER_ICON,
|
||||||
|
amenity_sanitary_dump_station: SANITARY_DISPOSAL_ICON,
|
||||||
|
amenity_toilets: RESTROOMS_ICON,
|
||||||
|
shop_gas: PROPANE_ICON,
|
||||||
|
amenity_electricity: ELECTRICAL_HOOKUP_ICON,
|
||||||
|
shop_laundry: LAUNDRY_ICON,
|
||||||
|
leisure_playground: PLAYGROUND_ICON,
|
||||||
|
tourism_viewpoint: SCENIC_VIEWPOINT_ICON,
|
||||||
|
leisure_swimming: SWIMMING_ICON,
|
||||||
|
leisure_fishing: FISHING_ICON,
|
||||||
|
sport_climbing: CLIMBING_ICON,
|
||||||
|
route_hiking: WALKING_ICON,
|
||||||
|
});
|
||||||
|
|
||||||
export const LIVETRACK_ICON = `
|
export const LIVETRACK_ICON = `
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M12 21s7-7.58 7-12a7 7 0 0 0-14 0c0 4.42 7 12 7 12z"/>
|
<path d="M12 21s7-7.58 7-12a7 7 0 0 0-14 0c0 4.42 7 12 7 12z"/>
|
||||||
|
|||||||
+69
-4
@@ -1,6 +1,31 @@
|
|||||||
import { NOTE_PIN_ICON } from "/js/icons.js";
|
import { Marker } from "/vendor/maplibre-gl/maplibre-gl.mjs";
|
||||||
|
import { NOTE_PIN_ICON, POI_ICONS } from "/js/icons.js";
|
||||||
import { notifyGpxStoreChanged } from "/js/panels.js";
|
import { notifyGpxStoreChanged } from "/js/panels.js";
|
||||||
|
|
||||||
|
// Human labels for the icon picker's title attribute — POI_ICONS' own keys
|
||||||
|
// are internal (OSM-tag-shaped) ids, not display text.
|
||||||
|
const ICON_LABELS = {
|
||||||
|
amenity_parking: "Parking",
|
||||||
|
tourism_camp_site: "Camp site",
|
||||||
|
leisure_picnic_table: "Picnic table",
|
||||||
|
amenity_fuel: "Fuel",
|
||||||
|
tourism_hotel: "Lodging",
|
||||||
|
forest: "Forest",
|
||||||
|
sport_cycling: "Cycling",
|
||||||
|
amenity_drinking_water: "Drinking water",
|
||||||
|
amenity_sanitary_dump_station: "Dump station",
|
||||||
|
amenity_toilets: "Toilets",
|
||||||
|
shop_gas: "Gas/LPG refill",
|
||||||
|
amenity_electricity: "Electricity",
|
||||||
|
shop_laundry: "Laundry",
|
||||||
|
leisure_playground: "Playground",
|
||||||
|
tourism_viewpoint: "Viewpoint",
|
||||||
|
leisure_swimming: "Swimming",
|
||||||
|
leisure_fishing: "Fishing",
|
||||||
|
sport_climbing: "Climbing",
|
||||||
|
route_hiking: "Hiking",
|
||||||
|
};
|
||||||
|
|
||||||
const NEW_LAYER_VALUE = "__new__";
|
const NEW_LAYER_VALUE = "__new__";
|
||||||
|
|
||||||
// Every hand-drawn note (from here or measure.js's "Save as note") gets one
|
// Every hand-drawn note (from here or measure.js's "Save as note") gets one
|
||||||
@@ -29,6 +54,15 @@ export class NoteSaveForm {
|
|||||||
this._el.hidden = true;
|
this._el.hidden = true;
|
||||||
this._el.innerHTML = `
|
this._el.innerHTML = `
|
||||||
<input type="text" class="notes-panel__title" placeholder="Title">
|
<input type="text" class="notes-panel__title" placeholder="Title">
|
||||||
|
<div class="notes-panel__icons">
|
||||||
|
<button type="button" class="notes-panel__icon-option active" data-icon="" title="No icon">${NOTE_PIN_ICON}</button>
|
||||||
|
${Object.keys(POI_ICONS)
|
||||||
|
.map(
|
||||||
|
(key) =>
|
||||||
|
`<button type="button" class="notes-panel__icon-option" data-icon="${key}" title="${escapeAttr(ICON_LABELS[key] || key)}">${POI_ICONS[key]}</button>`,
|
||||||
|
)
|
||||||
|
.join("")}
|
||||||
|
</div>
|
||||||
<textarea class="notes-panel__body" rows="3" placeholder="Details (optional) — **bold**, *italic*, links"></textarea>
|
<textarea class="notes-panel__body" rows="3" placeholder="Details (optional) — **bold**, *italic*, links"></textarea>
|
||||||
<select class="notes-panel__target"></select>
|
<select class="notes-panel__target"></select>
|
||||||
<input type="text" class="notes-panel__new-name" placeholder="Layer name" hidden>
|
<input type="text" class="notes-panel__new-name" placeholder="Layer name" hidden>
|
||||||
@@ -40,11 +74,19 @@ export class NoteSaveForm {
|
|||||||
container.appendChild(this._el);
|
container.appendChild(this._el);
|
||||||
|
|
||||||
this._title = this._el.querySelector(".notes-panel__title");
|
this._title = this._el.querySelector(".notes-panel__title");
|
||||||
|
this._iconPicker = this._el.querySelector(".notes-panel__icons");
|
||||||
this._body = this._el.querySelector(".notes-panel__body");
|
this._body = this._el.querySelector(".notes-panel__body");
|
||||||
this._target = this._el.querySelector(".notes-panel__target");
|
this._target = this._el.querySelector(".notes-panel__target");
|
||||||
this._newName = this._el.querySelector(".notes-panel__new-name");
|
this._newName = this._el.querySelector(".notes-panel__new-name");
|
||||||
this._saveButton = this._el.querySelector(".notes-panel__save");
|
this._saveButton = this._el.querySelector(".notes-panel__save");
|
||||||
|
this._selectedIcon = "";
|
||||||
|
|
||||||
|
this._iconPicker.querySelectorAll(".notes-panel__icon-option").forEach((button) => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
this._selectedIcon = button.dataset.icon;
|
||||||
|
this._iconPicker.querySelectorAll(".notes-panel__icon-option").forEach((b) => b.classList.toggle("active", b === button));
|
||||||
|
});
|
||||||
|
});
|
||||||
this._target.addEventListener("change", () => {
|
this._target.addEventListener("change", () => {
|
||||||
this._newName.hidden = this._target.value !== NEW_LAYER_VALUE;
|
this._newName.hidden = this._target.value !== NEW_LAYER_VALUE;
|
||||||
if (!this._newName.hidden) this._newName.focus();
|
if (!this._newName.hidden) this._newName.focus();
|
||||||
@@ -62,6 +104,12 @@ export class NoteSaveForm {
|
|||||||
this._el.hidden = false;
|
this._el.hidden = false;
|
||||||
this._title.value = "";
|
this._title.value = "";
|
||||||
this._body.value = "";
|
this._body.value = "";
|
||||||
|
this._selectedIcon = "";
|
||||||
|
this._iconPicker.querySelectorAll(".notes-panel__icon-option").forEach((b, i) => b.classList.toggle("active", i === 0));
|
||||||
|
// 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
|
||||||
|
// a badge glyph to sit at.
|
||||||
|
this._iconPicker.hidden = geometry.type !== "Point";
|
||||||
this._title.focus();
|
this._title.focus();
|
||||||
await this._populateTargets();
|
await this._populateTargets();
|
||||||
}
|
}
|
||||||
@@ -112,6 +160,7 @@ export class NoteSaveForm {
|
|||||||
category: "Note",
|
category: "Note",
|
||||||
time: new Date().toISOString(),
|
time: new Date().toISOString(),
|
||||||
...(details ? { description: details } : {}),
|
...(details ? { description: details } : {}),
|
||||||
|
...(this._selectedIcon ? { icon: this._selectedIcon } : {}),
|
||||||
},
|
},
|
||||||
geometry: this._geometry,
|
geometry: this._geometry,
|
||||||
};
|
};
|
||||||
@@ -143,6 +192,7 @@ export class NoteSaveForm {
|
|||||||
export class NotesControl {
|
export class NotesControl {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._active = false;
|
this._active = false;
|
||||||
|
this._marker = null; // where the pending note will land — visible until saved/cancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
onAdd(map) {
|
onAdd(map) {
|
||||||
@@ -161,8 +211,14 @@ export class NotesControl {
|
|||||||
this._hint = this._container.querySelector(".notes-panel__hint");
|
this._hint = this._container.querySelector(".notes-panel__hint");
|
||||||
|
|
||||||
this._form = new NoteSaveForm(this._panel, {
|
this._form = new NoteSaveForm(this._panel, {
|
||||||
onSaved: () => (this._hint.hidden = false),
|
onSaved: () => {
|
||||||
onCancel: () => (this._hint.hidden = false),
|
this._hint.hidden = false;
|
||||||
|
this._removeMarker();
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
this._hint.hidden = false;
|
||||||
|
this._removeMarker();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this._button.addEventListener("click", () => this._setActive(!this._active));
|
this._button.addEventListener("click", () => this._setActive(!this._active));
|
||||||
@@ -170,6 +226,7 @@ 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);
|
||||||
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);
|
||||||
@@ -177,6 +234,11 @@ export class NotesControl {
|
|||||||
return this._container;
|
return this._container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_removeMarker() {
|
||||||
|
this._marker?.remove();
|
||||||
|
this._marker = null;
|
||||||
|
}
|
||||||
|
|
||||||
onRemove() {
|
onRemove() {
|
||||||
this._map.off("click", this._onClick);
|
this._map.off("click", this._onClick);
|
||||||
this._container.remove();
|
this._container.remove();
|
||||||
@@ -191,7 +253,10 @@ export class NotesControl {
|
|||||||
this._button.classList.toggle("active", active);
|
this._button.classList.toggle("active", active);
|
||||||
this._panel.hidden = !active;
|
this._panel.hidden = !active;
|
||||||
this._map.getCanvas().style.cursor = active ? "crosshair" : "";
|
this._map.getCanvas().style.cursor = active ? "crosshair" : "";
|
||||||
if (!active) this._form.close();
|
if (!active) {
|
||||||
|
this._form.close();
|
||||||
|
this._removeMarker();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user