forked from MapComplete/MapComplete
A11y: move buttons into fields
This commit is contained in:
parent
30c9034e7b
commit
1b10f1f64d
23 changed files with 529 additions and 414 deletions
|
@ -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()}"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)]}
|
||||
}))
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -51,4 +51,8 @@ export default class OpeningHoursValidator extends Validator {
|
|||
])
|
||||
)
|
||||
}
|
||||
|
||||
reformat(s: string, _?: () => string): string {
|
||||
return super.reformat(s, _)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
111
src/UI/Popup/Notes/AddNoteComment.svelte
Normal file
111
src/UI/Popup/Notes/AddNoteComment.svelte
Normal 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>
|
26
src/UI/Popup/Notes/AddNoteCommentViz.ts
Normal file
26
src/UI/Popup/Notes/AddNoteCommentViz.ts
Normal 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 })
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -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">
|
|
@ -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()
|
|
@ -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}
|
||||
|
|
|
@ -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} />
|
||||
|
|
|
@ -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 }>
|
||||
|
|
|
@ -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")
|
||||
})
|
||||
),
|
||||
|
|
|
@ -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) {
|
||||
|
|
11
src/Utils.ts
11
src/Utils.ts
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
/**
|
||||
|
|
Loading…
Reference in a new issue