A11y: move buttons into fields

This commit is contained in:
Pieter Vander Vennet 2023-12-26 22:30:27 +01:00
parent 30c9034e7b
commit 1b10f1f64d
23 changed files with 529 additions and 414 deletions

View file

@ -54,6 +54,10 @@
},
"titleIcons": [
{
"ariaLabel": {
"en": "See on OpenStreetMap.org",
"nl": "Bekijk op OpenStreetMap.org"
},
"render": "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>"
}
],
@ -94,6 +98,7 @@
"tagRenderings": [
{
"id": "conversation",
"classes": "p-0",
"render": "{visualize_note_comments()}"
},
{

View file

@ -1118,14 +1118,14 @@ video {
height: 50%;
}
.h-3 {
height: 0.75rem;
}
.h-7 {
height: 1.75rem;
}
.h-3 {
height: 0.75rem;
}
.h-11 {
height: 2.75rem;
}
@ -1142,18 +1142,6 @@ video {
height: 12rem;
}
.h-56 {
height: 14rem;
}
.h-20 {
height: 5rem;
}
.h-10 {
height: 2.5rem;
}
.h-40 {
height: 10rem;
}
@ -1162,10 +1150,22 @@ video {
height: 16rem;
}
.h-10 {
height: 2.5rem;
}
.h-80 {
height: 20rem;
}
.h-56 {
height: 14rem;
}
.h-20 {
height: 5rem;
}
.max-h-12 {
max-height: 3rem;
}
@ -1178,10 +1178,6 @@ video {
max-height: 16rem;
}
.max-h-7 {
max-height: 1.75rem;
}
.max-h-60 {
max-height: 15rem;
}
@ -1228,14 +1224,14 @@ video {
width: 1rem;
}
.w-3 {
width: 0.75rem;
}
.w-7 {
width: 1.75rem;
}
.w-3 {
width: 0.75rem;
}
.w-11 {
width: 2.75rem;
}
@ -1614,11 +1610,6 @@ video {
border-radius: 0.125rem;
}
.rounded-l {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
.rounded-t {
border-top-left-radius: 0.25rem;
border-top-right-radius: 0.25rem;
@ -1634,6 +1625,11 @@ video {
border-bottom-left-radius: 0.25rem;
}
.rounded-l {
border-top-left-radius: 0.25rem;
border-bottom-left-radius: 0.25rem;
}
.rounded-tl {
border-top-left-radius: 0.25rem;
}
@ -1843,6 +1839,11 @@ video {
padding-right: 0.75rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
.pr-2 {
padding-right: 0.5rem;
}
@ -2317,6 +2318,14 @@ input[type=text] {
width: 100%;
}
.debug input, .debug textarea {
border: 6px solid red
}
.debug label input, .debug label textarea {
border: 1px solid grey;
}
/************************* BIG CATEGORIES ********************************/
/**
@ -2490,6 +2499,12 @@ button.link:hover {
fill: var(--foreground-color) !important;
}
.neutral-label{
/** This label styles as normal text. It's power comes from the many :not(.neutral-label) entries.
* Placed here for autocompletion
*/
}
label:not(.neutral-label) {
/**
* Label should _contain_ the input element
@ -2924,14 +2939,6 @@ a.link-underline {
margin-right: 1rem;
}
.sm\:mr-2 {
margin-right: 0.5rem;
}
.sm\:flex {
display: flex;
}
.sm\:h-24 {
height: 6rem;
}
@ -2948,14 +2955,6 @@ a.link-underline {
flex-wrap: nowrap;
}
.sm\:items-stretch {
align-items: stretch;
}
.sm\:justify-between {
justify-content: space-between;
}
.sm\:border-4 {
border-width: 4px;
}

View file

@ -7,7 +7,7 @@ import { Store, UIEventSource } from "../UIEventSource"
import { OsmConnection } from "../Osm/OsmConnection"
import { Changes } from "../Osm/Changes"
import Translations from "../../UI/i18n/Translations"
import NoteCommentElement from "../../UI/Popup/NoteCommentElement"
import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement"
/**
* The ImageUploadManager has a

View file

@ -23,7 +23,10 @@ export class LocalStorageSource {
static Get(key: string, defaultValue: string = undefined): UIEventSource<string> {
try {
const saved = localStorage.getItem(key)
let saved = localStorage.getItem(key)
if (saved === "undefined") {
saved = undefined
}
const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:" + key)
source.addCallback((data) => {

View file

@ -647,6 +647,12 @@ export default class TagRenderingConfig {
multiSelectedMapping: boolean[] | undefined,
currentProperties: Record<string, string>
): UploadableTag {
console.log("Constructing change spec", {
freeformValue,
singleSelectedMapping,
multiSelectedMapping,
currentProperties,
})
if (typeof freeformValue === "string") {
freeformValue = freeformValue?.trim()
}

View file

@ -3,6 +3,8 @@
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
import { twMerge } from "tailwind-merge"
import { trapFocus } from "trap-focus-svelte"
import { ariaLabel } from "../../Utils/ariaLabel"
import Translations from "../i18n/Translations"
/**
* The slotted element will be shown on top, with a lower-opacity border
@ -35,6 +37,7 @@
<button
class="absolute right-10 top-10 h-8 w-8 cursor-pointer rounded-full border-none bg-white p-0"
on:click={() => dispatch("close")}
use:ariaLabel={Translations.t.general.backToMap}
>
<XCircleIcon />
</button>

View file

@ -38,7 +38,7 @@
return {bearing, distance: distanceToCurrentLocation.data.distance}
}, [distanceToCurrentLocation])
let viewportCenterDetails = Translations.DynamicSubstitute(t.viewportCenterDetails, relativeBearing)
let viewportCenterDetailsAbsolute = Translations.DynamicSubstitute(t.viewportCenterDetails, distanceToCurrentLocation.map(({distance, bearing}) => {
let viewportCenterDetailsAbsolute = Translations.DynamicSubstitute(t.viewportCenterDetails, distanceToCurrentLocation.mapD(({distance, bearing}) => {
return {distance, bearing: t.directionsAbsolute[GeoOperations.bearingToHuman(bearing)]}
}))

View file

@ -52,10 +52,9 @@
if (maxDistanceInMeters) {
onDestroy(
mla.location.addCallbackD((newLocation) => {
const l = [newLocation.lon, newLocation.lat]
const l : [number, number] = [newLocation.lon, newLocation.lat]
const c: [number, number] = [initialCoordinate.lon, initialCoordinate.lat]
const d = GeoOperations.distanceBetween(l, c)
console.log("distance is", d, l, c)
if (d <= maxDistanceInMeters) {
return
}

View file

@ -14,8 +14,9 @@
export let type: ValidatorType
export let feedback: UIEventSource<Translation> | undefined = undefined
export let cls: string = undefined
export let getCountry: () => string | undefined
export let placeholder: string | Translation | undefined
export let getCountry: () => string | undefined = undefined
export let placeholder: string | Translation | undefined = undefined
export let autofocus: boolean = false
export let unit: Unit = undefined
/**
* Valid state, exported to the calling component
@ -57,9 +58,9 @@
validator = Validators.get(type ?? "string")
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
if(_value.data?.length > 0){
if (_value.data?.length > 0) {
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
}else{
} else {
feedback?.setData(undefined)
}
@ -69,7 +70,7 @@
function setValues() {
// Update the value stores
const v = _value.data
if(v === ""){
if (v === "") {
value.setData(undefined)
feedback?.setData(undefined)
return
@ -100,7 +101,7 @@
if (_value.data !== fromUpstream && fromUpstream !== "") {
_value.setData(fromUpstream)
}
})
}),
)
} else {
// Handled by the UnitInput
@ -114,7 +115,7 @@
Utils.sortedByLevenshteinDistance(
type,
Validators.AllValidators.map((v) => v.name),
(v) => v
(v) => v,
)
.slice(0, 5)
.join(", ")
@ -123,37 +124,30 @@
const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true)
let htmlElem: HTMLInputElement
let htmlElem: HTMLInputElement | HTMLTextAreaElement
let dispatch = createEventDispatcher<{ selected; submit }>()
let dispatch = createEventDispatcher<{ selected }>()
$: {
if (htmlElem !== undefined) {
htmlElem.onfocus = () => dispatch("selected")
if (autofocus) {
Utils.focusOn(htmlElem)
}
}
}
/**
* Dispatches the submit, but only if the value is valid
*/
function sendSubmit() {
if (feedback?.data) {
console.log("Not sending a submit as there is feedback")
}
dispatch("submit")
}
</script>
{#if validator?.textArea}
<form on:submit|preventDefault={() => sendSubmit()}>
<textarea
class="w-full"
bind:value={$_value}
inputmode={validator?.inputmode ?? "text"}
placeholder={_placeholder}
bind:this={htmlElem}
/>
</form>
{:else}
<form class={twMerge("inline-flex", cls)} on:submit|preventDefault={() => sendSubmit()}>
<div class={twMerge("inline-flex", cls)}>
<input
bind:this={htmlElem}
bind:value={$_value}
@ -168,5 +162,5 @@
{#if unit !== undefined}
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} {getCountry} />
{/if}
</form>
</div>
{/if}

View file

@ -51,4 +51,8 @@ export default class OpeningHoursValidator extends Validator {
])
)
}
reformat(s: string, _?: () => string): string {
return super.reformat(s, _)
}
}

View file

@ -1,120 +0,0 @@
import Translations from "../i18n/Translations"
import { TextField } from "../Input/TextField"
import { SubtleButton } from "../Base/SubtleButton"
import Svg from "../../Svg"
import NoteCommentElement from "./NoteCommentElement"
import { VariableUiElement } from "../Base/VariableUIElement"
import Toggle from "../Input/Toggle"
import { LoginToggle } from "./LoginButton"
import Combine from "../Base/Combine"
import Title from "../Base/Title"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
export class AddNoteCommentViz implements SpecialVisualization {
funcName = "add_note_comment"
needsUrls = [Constants.osmAuthConfig.url]
docs = "A textfield to add a comment to a node (with the option to close the note)."
args = [
{
name: "Id-key",
doc: "The property name where the ID of the note to close can be found",
defaultValue: "id",
},
]
public constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[]
) {
const t = Translations.t.notes
const textField = new TextField({
placeholder: t.addCommentPlaceholder,
inputStyle: "width: 100%; height: 6rem;",
textAreaRows: 3,
htmlType: "area",
})
textField.SetClass("rounded-l border border-grey")
const txt = textField.GetValue()
const addCommentButton = new SubtleButton(
Svg.speech_bubble_svg().SetClass("max-h-7"),
t.addCommentPlaceholder
).onClick(async () => {
const id = tags.data[args[1] ?? "id"]
if ((txt.data ?? "") == "") {
return
}
if (isClosed.data) {
await state.osmConnection.reopenNote(id, txt.data)
await state.osmConnection.closeNote(id)
} else {
await state.osmConnection.addCommentToNote(id, txt.data)
}
NoteCommentElement.addCommentTo(txt.data, tags, state)
txt.setData("")
})
const close = new SubtleButton(
Svg.resolved_svg().SetClass("max-h-7"),
new VariableUiElement(
txt.map((txt) => {
if (txt === undefined || txt === "") {
return t.closeNote
}
return t.addCommentAndClose
})
)
).onClick(async () => {
const id = tags.data[args[1] ?? "id"]
await state.osmConnection.closeNote(id, txt.data)
tags.data["closed_at"] = new Date().toISOString()
tags.ping()
})
const reopen = new SubtleButton(
Svg.note_svg().SetClass("max-h-7"),
new VariableUiElement(
txt.map((txt) => {
if (txt === undefined || txt === "") {
return t.reopenNote
}
return t.reopenNoteAndComment
})
)
).onClick(async () => {
const id = tags.data[args[1] ?? "id"]
await state.osmConnection.reopenNote(id, txt.data)
tags.data["closed_at"] = undefined
tags.ping()
})
const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "")
const stateButtons = new Toggle(
new Toggle(reopen, close, isClosed),
undefined,
state.osmConnection.isLoggedIn
)
return new LoginToggle(
new Combine([
new Title(t.addAComment),
textField,
new Combine([
stateButtons.SetClass("sm:mr-2"),
new Toggle(
addCommentButton,
new Combine([t.typeText]).SetClass("flex items-center h-full subtle"),
textField.GetValue().map((t) => t !== undefined && t.length >= 1)
).SetClass("sm:mr-2"),
]).SetClass("sm:flex sm:justify-between sm:items-stretch"),
]).SetClass("border-2 border-black rounded-xl p-4 block"),
t.loginToAddComment,
state
)
}
}

View file

@ -0,0 +1,111 @@
<script lang="ts">
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import { Store, UIEventSource } from "../../../Logic/UIEventSource"
import { placeholder } from "../../../Utils/placeholder"
import Translations from "../../i18n/Translations"
import Speech_bubble from "../../../assets/svg/Speech_bubble.svelte"
import Tr from "../../Base/Tr.svelte"
import NoteCommentElement from "./NoteCommentElement"
import Resolved from "../../../assets/svg/Resolved.svelte"
import Note from "../../../assets/svg/Note.svelte"
import LoginToggle from "../../Base/LoginToggle.svelte"
export let state: SpecialVisualizationState
export let tags: UIEventSource<Record<string, string>>
let id = tags.data.id
$: {
id = $tags.id
}
let txt = new UIEventSource(undefined)
let _txt: string = undefined
txt.addCallbackD(t => {
_txt = t
})
$: {
txt.setData(_txt)
}
const t = Translations.t.notes
let isClosed: Store<boolean> = tags.map((tags) => (tags?.["closed_at"] ?? "") !== "")
async function addComment() {
if ((txt.data ?? "") == "") {
return
}
if (isClosed.data) {
await state.osmConnection.reopenNote(id, txt.data)
await state.osmConnection.closeNote(id)
} else {
await state.osmConnection.addCommentToNote(id, txt.data)
}
NoteCommentElement.addCommentTo(txt.data, tags, state)
txt.setData("")
}
async function closeNote() {
await state.osmConnection.closeNote(id, txt.data)
tags.data["closed_at"] = new Date().toISOString()
tags.ping()
}
async function reopenNote() {
await state.osmConnection.reopenNote(id, txt.data)
tags.data["closed_at"] = undefined
tags.ping()
}
</script>
<LoginToggle ignoreLoading={true} {state}>
<Tr slot="not-logged-in" t={t.loginToAddComment} />
<form class="m-0 px-2 py-1 flex flex-col border-2 border-black rounded-xl interactive border-interactive" on:submit|preventDefault={() => addComment()}>
<label class="neutral-label font-bold">
<Tr t={t.addAComment} />
<textarea bind:value={_txt} class="w-full h-24 rounded-l border border-grey" rows="3"
use:placeholder={t.addCommentPlaceholder} />
</label>
<div class="flex flex-col">
{#if $txt?.length > 0}
<button class="primary flex" on:click={() => addComment()}>
<!-- Add a comment -->
<Speech_bubble class="h-7 w-7 pr-2" />
<Tr t={t.addCommentPlaceholder} />
</button>
{:else}
<div class="alert w-full">
<Tr t={t.typeText} />
</div>
{/if}
{#if !$isClosed}
<button class="flex items-center" on:click={() => closeNote()}>
<Resolved class="h-8 w-8 pr-2" />
<!-- Close note -->
{#if $txt === undefined || $txt === ""}
<Tr t={t.closeNote} />
{:else}
<Tr t={t.addCommentAndClose} />
{/if}
</button>
{:else}
<button class="flex items-center" on:click={() => reopenNote()}>
<!-- Reopen -->
<Note class="h-7 w-7 pr-2" />
{#if $txt === undefined || $txt === ""}
<Tr t={t.reopenNote} />
{:else}
<Tr t={t.reopenNoteAndComment} />
{/if}
</button>
{/if}
</div>
</form>
</LoginToggle>

View file

@ -0,0 +1,26 @@
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
import { UIEventSource } from "../../../Logic/UIEventSource"
import Constants from "../../../Models/Constants"
import SvelteUIElement from "../../Base/SvelteUIElement"
import AddNoteComment from "./AddNoteComment.svelte"
export class AddNoteCommentViz implements SpecialVisualization {
funcName = "add_note_comment"
needsUrls = [Constants.osmAuthConfig.url]
docs = "A textfield to add a comment to a node (with the option to close the note)."
args = [
{
name: "Id-key",
doc: "The property name where the ID of the note to close can be found",
defaultValue: "id",
},
]
public constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[]
) {
return new SvelteUIElement(AddNoteComment, { state, tags })
}
}

View file

@ -1,14 +1,14 @@
import BaseUIElement from "../BaseUIElement"
import Translations from "../i18n/Translations"
import { Utils } from "../../Utils"
import Svg from "../../Svg"
import Img from "../Base/Img"
import { SubtleButton } from "../Base/SubtleButton"
import Toggle from "../Input/Toggle"
import { LoginToggle } from "./LoginButton"
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import Constants from "../../Models/Constants"
import BaseUIElement from "../../BaseUIElement"
import Translations from "../../i18n/Translations"
import { Utils } from "../../../Utils"
import Svg from "../../../Svg"
import Img from "../../Base/Img"
import { SubtleButton } from "../../Base/SubtleButton"
import Toggle from "../../Input/Toggle"
import { LoginToggle } from ".././LoginButton"
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
import { UIEventSource } from "../../../Logic/UIEventSource"
import Constants from "../../../Models/Constants"
export class CloseNoteButton implements SpecialVisualization {
public readonly funcName = "close_note"

View file

@ -2,21 +2,23 @@
/**
* UIcomponent to create a new note at the given location
*/
import type { SpecialVisualizationState } from "../SpecialVisualization"
import { UIEventSource } from "../../Logic/UIEventSource"
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
import ValidatedInput from "../InputElement/ValidatedInput.svelte"
import SubtleButton from "../Base/SubtleButton.svelte"
import Tr from "../Base/Tr.svelte"
import Translations from "../i18n/Translations.js"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import { UIEventSource } from "../../../Logic/UIEventSource"
import { LocalStorageSource } from "../../../Logic/Web/LocalStorageSource"
import ValidatedInput from "../../InputElement/ValidatedInput.svelte"
import SubtleButton from "../../Base/SubtleButton.svelte"
import Tr from "../../Base/Tr.svelte"
import Translations from "../../i18n/Translations.js"
import type { Feature, Point } from "geojson"
import LoginToggle from "../Base/LoginToggle.svelte"
import FilteredLayer from "../../Models/FilteredLayer"
import NewPointLocationInput from "../BigComponents/NewPointLocationInput.svelte"
import ToSvelte from "../Base/ToSvelte.svelte"
import Svg from "../../Svg"
import Layers from "../../assets/svg/Layers.svelte"
import AddSmall from "../../assets/svg/AddSmall.svelte"
import LoginToggle from "../../Base/LoginToggle.svelte"
import FilteredLayer from "../../../Models/FilteredLayer"
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"
import ToSvelte from "../../Base/ToSvelte.svelte"
import Svg from "../../../Svg"
import Layers from "../../../assets/svg/Layers.svelte"
import AddSmall from "../../../assets/svg/AddSmall.svelte"
import type { OsmTags } from "../../../Models/OsmFeature"
import Loading from "../../Base/Loading.svelte"
export let coordinate: UIEventSource<{ lon: number; lat: number }>
export let state: SpecialVisualizationState
@ -29,12 +31,14 @@
let hasFilter = notelayer?.hasFilter
let isDisplayed = notelayer?.isDisplayed
let submitted = false
function enableNoteLayer() {
state.guistate.closeAll()
isDisplayed.setData(true)
}
async function uploadNote() {
submitted = true
let txt = comment.data
if (txt === undefined || txt === "") {
return
@ -43,7 +47,7 @@
txt += "\n\n #MapComplete #" + state?.layout?.id
const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt)
console.log("Created a note, got id", id)
const feature = <Feature<Point>>{
const feature = <Feature<Point, OsmTags>>{
type: "Feature",
geometry: {
type: "Point",
@ -73,7 +77,6 @@
comment.setData("")
created = true
state.selectedElement.setData(feature)
state.selectedLayer.setData(state.layerState.filteredLayers.get("note"))
}
</script>
@ -81,6 +84,8 @@
<div class="alert">
This theme does not include the layer 'note'. As a result, no nodes can be created
</div>
{:else if submitted}
<Loading/>
{:else if created}
<div class="thanks">
<Tr t={Translations.t.notes.isCreated} />
@ -104,40 +109,41 @@
</SubtleButton>
</div>
{:else}
<div>
<Tr t={Translations.t.notes.createNoteIntro} />
<div class="border-grey-500 rounded-sm border">
<form class="border-grey-500 rounded-sm border" on:submit|preventDefault={uploadNote}>
<label class="neutral-label">
<Tr t={Translations.t.notes.createNoteIntro} />
<div class="w-full p-1">
<ValidatedInput type="text" value={comment} />
<ValidatedInput autofocus={true} type="text" value={comment} />
</div>
</label>
<div class="h-56 w-full">
<NewPointLocationInput value={coordinate} {state}>
<div class="h-20 w-full pb-10" slot="image">
<ToSvelte construct={Svg.note_svg().SetClass("h-10 w-full")} />
</div>
</NewPointLocationInput>
</div>
<LoginToggle {state}>
<span slot="loading"><!--empty: don't show a loading message--></span>
<div slot="not-logged-in" class="alert">
<Tr t={Translations.t.notes.warnAnonymous} />
<div class="h-56 w-full">
<NewPointLocationInput value={coordinate} {state}>
<div class="h-20 w-full pb-10" slot="image">
<ToSvelte construct={Svg.note_svg().SetClass("h-10 w-full")} />
</div>
</LoginToggle>
{#if $comment?.length >= 3}
<SubtleButton on:click={uploadNote}>
<AddSmall slot="image" class="mr-4 h-8 w-8" />
<Tr slot="message" t={Translations.t.notes.createNote} />
</SubtleButton>
{:else}
<div class="alert">
<Tr t={Translations.t.notes.textNeeded} />
</div>
{/if}
</NewPointLocationInput>
</div>
</div>
<LoginToggle {state}>
<span slot="loading"><!--empty: don't show a loading message--></span>
<div slot="not-logged-in" class="alert">
<Tr t={Translations.t.notes.warnAnonymous} />
</div>
</LoginToggle>
{#if $comment?.length >= 3}
<SubtleButton on:click={uploadNote}>
<AddSmall slot="image" class="mr-4 h-8 w-8" />
<Tr slot="message" t={Translations.t.notes.createNote} />
</SubtleButton>
{:else}
<div class="alert">
<Tr t={Translations.t.notes.textNeeded} />
</div>
{/if}
</form>
{/if}
{:else}
<div class="flex flex-col">

View file

@ -1,26 +1,33 @@
import Combine from "../Base/Combine"
import BaseUIElement from "../BaseUIElement"
import Svg from "../../Svg"
import Link from "../Base/Link"
import { FixedUiElement } from "../Base/FixedUiElement"
import Translations from "../i18n/Translations"
import { Utils } from "../../Utils"
import Img from "../Base/Img"
import { SlideShow } from "../Image/SlideShow"
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import { VariableUiElement } from "../Base/VariableUIElement"
import Combine from "../../Base/Combine"
import BaseUIElement from "../../BaseUIElement"
import Svg from "../../../Svg"
import Link from "../../Base/Link"
import { FixedUiElement } from "../../Base/FixedUiElement"
import Translations from "../../i18n/Translations"
import { Utils } from "../../../Utils"
import Img from "../../Base/Img"
import { SlideShow } from "../../Image/SlideShow"
import { Stores, UIEventSource } from "../../../Logic/UIEventSource"
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
import { VariableUiElement } from "../../Base/VariableUIElement"
import { SpecialVisualizationState } from "../../SpecialVisualization"
export default class NoteCommentElement extends Combine {
constructor(comment: {
date: string
uid: number
user: string
user_url: string
action: "closed" | "opened" | "reopened" | "commented"
text: string
html: string
}) {
constructor(
comment: {
date: string
uid: number
user: string
user_url: string
action: "closed" | "opened" | "reopened" | "commented"
text: string
html: string
highlighted: boolean
},
state?: SpecialVisualizationState,
index?: number,
totalNumberOfComments?: number
) {
const t = Translations.t.notes
let actionIcon: BaseUIElement
@ -68,7 +75,15 @@ export default class NoteCommentElement extends Combine {
let imagesEl: BaseUIElement = undefined
if (images.length > 0) {
const imageEls = images.map((i) =>
new Img(i).SetClass("w-full block").SetStyle("min-width: 50px; background: grey;")
new Img(i)
.SetClass("w-full block cursor-pointer")
.onClick(() =>
state?.previewedImage?.setData(<any>{
url_hd: i,
url: i,
})
)
.SetStyle("min-width: 50px; background: grey;")
)
imagesEl = new SlideShow(new UIEventSource<BaseUIElement[]>(imageEls)).SetClass("mb-1")
}
@ -84,6 +99,16 @@ export default class NoteCommentElement extends Combine {
),
])
this.SetClass("flex flex-col pb-2 mb-2 border-gray-500 border-b")
if (comment.highlighted) {
this.SetClass("glowing-shadow")
console.log(">>>", index, totalNumberOfComments)
if (index + 2 === totalNumberOfComments) {
console.log("Scrolling into view")
requestAnimationFrame(() => {
this.ScrollIntoView()
})
}
}
}
public static addCommentTo(
@ -107,6 +132,7 @@ export default class NoteCommentElement extends Combine {
action: "commented",
text: txt,
html: html,
highlighted: true,
})
tags.data["comments"] = JSON.stringify(comments)
tags.ping()

View file

@ -105,7 +105,9 @@
}
// TODO this has _to much_ values
freeformInput.setData(unseenFreeformValues.join(";"))
checkedMappings.push(unseenFreeformValues.length > 0)
if(checkedMappings.length + 1 < mappings.length ){
checkedMappings.push(unseenFreeformValues.length > 0)
}
}
}
if (confg.freeform?.key) {
@ -125,13 +127,31 @@
initialize($tags, config)
}
freeformInput.addCallbackAndRun(freeformValue => {
console.log("FreeformValue:", freeformValue)
if (!mappings || mappings?.length == 0 || config.freeform?.key === undefined) {
return
}
// If a freeform value is given, mark the 'mapping' as marked
if (config.multiAnswer) {
if (checkedMappings === undefined) {
// Initialization didn't yet run
return
}
checkedMappings[mappings.length] = freeformValue?.length > 0
return
}
if (freeformValue?.length > 0) {
selectedMapping = mappings.length
}
})
$: {
try {
selectedTags = config?.constructChangeSpecification(
$freeformInput,
selectedMapping,
checkedMappings,
tags.data
tags.data,
)
} catch (e) {
console.error("Could not calculate changeSpecification:", e)
@ -182,19 +202,6 @@
}
}
$: {
try {
selectedTags = config?.constructChangeSpecification(
$freeformInput,
selectedMapping,
checkedMappings,
tags.data
)
} catch (e) {
console.error("Could not calculate changeSpecification:", e)
selectedTags = undefined
}
}
let featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false)
let featureSwitchIsDebugging =
@ -207,148 +214,151 @@
onDestroy(
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
numberOfCs = ud.csCount
})
}),
)
}
</script>
{#if question !== undefined}
<div
<form
class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
style="max-height: 75vh"
on:submit|preventDefault={() => onSave()}
>
<div class="interactive sticky top-0 flex justify-between pt-1" style="z-index: 11">
<label class="neutral-label">
<div class="interactive sticky top-0 flex justify-between pt-1" style="z-index: 11">
<span class="font-bold">
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
</span>
<slot name="upper-right" />
</div>
<slot name="upper-right" />
</div>
{#if config.questionhint}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{#if config.questionhint}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
{#if config.mappings?.length >= 8}
<div class="sticky flex w-full" aria-hidden="true">
<Search class="h-6 w-6" />
<input
type="text"
bind:value={$searchTerm}
class="w-full"
use:placeholder={Translations.t.general.searchAnswer}
/>
</div>
{/if}
{#if config.freeform?.key && !(mappings?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
{layer}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}
/>
</div>
{/if}
{#if config.mappings?.length >= 8}
<div class="sticky flex w-full" aria-hidden="true">
<Search class="h-6 w-6" />
<input
type="text"
bind:value={$searchTerm}
class="w-full"
use:placeholder={Translations.t.general.searchAnswer}
/>
</div>
{/if}
{#if config.freeform?.key && !(mappings?.length > 0)}
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}
/>
{:else if mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
<TagRenderingMappingInput
{mapping}
{tags}
{state}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={selectedMapping === i}
>
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={i}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex">
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={config.mappings?.length}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{:else if mappings !== undefined && !config.multiAnswer}
<!-- Simple radiobuttons as mapping -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
<TagRenderingMappingInput
{mapping}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:selected={() => (selectedMapping = config.mappings?.length)}
on:submit={onSave}
/>
</label>
{/if}
</div>
{:else if mappings !== undefined && config.multiAnswer}
<!-- Multiple answers can be chosen: checkboxes -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<TagRenderingMappingInput
{mapping}
{tags}
{state}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={checkedMappings[i]}
>
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + i}
bind:checked={checkedMappings[i]}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex">
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={selectedMapping === i}
>
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={i}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex">
<input
type="radio"
bind:group={selectedMapping}
name={"mappings-radio-" + config.id}
value={config.mappings?.length}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:selected={() => (selectedMapping = config.mappings?.length)}
on:submit={onSave}
/>
</label>
{/if}
</div>
{:else if mappings !== undefined && config.multiAnswer}
<!-- Multiple answers can be chosen: checkboxes -->
<div class="flex flex-col">
{#each config.mappings as mapping, i (mapping.then)}
<TagRenderingMappingInput
{mapping}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}
/>
</label>
{/if}
</div>
{/if}
{selectedElement}
{layer}
{searchTerm}
mappingIsSelected={checkedMappings[i]}
>
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + i}
bind:checked={checkedMappings[i]}
on:keypress={(e) => onInputKeypress(e)}
/>
</TagRenderingMappingInput>
{/each}
{#if config.freeform?.key}
<label class="flex">
<input
type="checkbox"
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
bind:checked={checkedMappings[config.mappings.length]}
on:keypress={(e) => onInputKeypress(e)}
/>
<FreeformInput
{config}
{tags}
{feedback}
{unit}
{state}
feature={selectedElement}
value={freeformInput}
on:submit={onSave}
/>
</label>
{/if}
</div>
{/if}
</label>
<LoginToggle {state}>
<Loading slot="loading" />
@ -391,5 +401,5 @@
{/if}
<slot name="under-buttons" />
</LoginToggle>
</div>
</form>
{/if}

View file

@ -85,9 +85,12 @@
/>
{#if confirmedScore !== undefined}
<label class="neutral-label">
<Tr cls="font-bold mt-2" t={t.question_opinion} />
<textarea autofocus bind:value={$opinion} inputmode="text" rows="3" class="mb-1 w-full"
use:placeholder={t.reviewPlaceholder}/>
</label>
<Checkbox selected={isAffiliated}>
<div class="flex flex-col">
<Tr t={t.i_am_affiliated} />

View file

@ -57,6 +57,9 @@ export interface SpecialVisualizationState {
readonly selectedElement: UIEventSource<Feature>
/**
* Works together with 'selectedElement' to indicate what properties should be displayed
* @deprecated
*
* No need to set this anymore
*/
readonly selectedLayer: UIEventSource<LayerConfig>
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>

View file

@ -13,10 +13,10 @@ import { MinimapViz } from "./Popup/MinimapViz"
import { ShareLinkViz } from "./Popup/ShareLinkViz"
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
import { MultiApplyViz } from "./Popup/MultiApplyViz"
import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"
import { AddNoteCommentViz } from "./Popup/Notes/AddNoteCommentViz"
import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
import TagApplyButton from "./Popup/TagApplyButton"
import { CloseNoteButton } from "./Popup/CloseNoteButton"
import { CloseNoteButton } from "./Popup/Notes/CloseNoteButton"
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
@ -30,7 +30,7 @@ import Translations from "./i18n/Translations"
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
import { SubtleButton } from "./Base/SubtleButton"
import Svg from "../Svg"
import NoteCommentElement from "./Popup/NoteCommentElement"
import NoteCommentElement from "./Popup/Notes/NoteCommentElement"
import { SubstitutedTranslation } from "./SubstitutedTranslation"
import List from "./Base/List"
import StatisticsPanel from "./BigComponents/StatisticsPanel"
@ -42,7 +42,7 @@ import SvelteUIElement from "./Base/SvelteUIElement"
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
import { Feature } from "geojson"
import { GeoOperations } from "../Logic/GeoOperations"
import CreateNewNote from "./Popup/CreateNewNote.svelte"
import CreateNewNote from "./Popup/Notes/CreateNewNote.svelte"
import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
import UserProfile from "./BigComponents/UserProfile.svelte"
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
@ -1004,7 +1004,10 @@ export default class SpecialVisualizations {
return new Combine(
comments
.filter((c) => c.text !== "")
.map((c) => new NoteCommentElement(c))
.map(
(c, i) =>
new NoteCommentElement(c, state, i, comments.length)
)
).SetClass("flex flex-col")
})
),

View file

@ -67,6 +67,7 @@
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
import { BBox } from "../Logic/BBox"
import { MapLibreAdaptor } from "./Map/MapLibreAdaptor.js"
import { QueryParameters } from "../Logic/Web/QueryParameters"
export let state: ThemeViewState
let layout = state.layout
@ -139,6 +140,15 @@
)
let previewedImage = state.previewedImage
let debug = state.featureSwitches.featureSwitchIsDebugging
debug.addCallbackAndRun(dbg => {
if(dbg){
document.body.classList.add("debug")
}else{
document.body.classList.remove("debug")
}
})
function forwardEventToMap(e: KeyboardEvent) {
const mlmap = state.map.data
if (!mlmap) {

View file

@ -1638,13 +1638,22 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
return newObj
}
public static focusOn(el: HTMLElement): void {
if (!el) {
return
}
requestAnimationFrame(() => {
el.focus()
})
}
/**
* Searches a child that can be focused on, by first selecting a 'focusable', then a button, then a link
*
* Returns the focussed element
* @param el
*/
public static focusOnFocusableChild(el: HTMLElement): undefined {
public static focusOnFocusableChild(el: HTMLElement): void {
if (!el) {
return
}

View file

@ -121,6 +121,16 @@ input[type=text] {
width: 100%;
}
.debug input, .debug textarea {
border: 6px solid red
}
.debug label input, .debug label textarea {
border: 1px solid grey;
}
/************************* BIG CATEGORIES ********************************/
/**
@ -302,6 +312,11 @@ button.link:hover {
fill: var(--foreground-color) !important;
}
.neutral-label{
/** This label styles as normal text. It's power comes from the many :not(.neutral-label) entries.
* Placed here for autocompletion
*/
}
label:not(.neutral-label) {
/**