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": [
|
"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>"
|
"render": "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -94,6 +98,7 @@
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
{
|
{
|
||||||
"id": "conversation",
|
"id": "conversation",
|
||||||
|
"classes": "p-0",
|
||||||
"render": "{visualize_note_comments()}"
|
"render": "{visualize_note_comments()}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -1118,14 +1118,14 @@ video {
|
||||||
height: 50%;
|
height: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-3 {
|
|
||||||
height: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-7 {
|
.h-7 {
|
||||||
height: 1.75rem;
|
height: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-3 {
|
||||||
|
height: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-11 {
|
.h-11 {
|
||||||
height: 2.75rem;
|
height: 2.75rem;
|
||||||
}
|
}
|
||||||
|
@ -1142,18 +1142,6 @@ video {
|
||||||
height: 12rem;
|
height: 12rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h-56 {
|
|
||||||
height: 14rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-20 {
|
|
||||||
height: 5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-10 {
|
|
||||||
height: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h-40 {
|
.h-40 {
|
||||||
height: 10rem;
|
height: 10rem;
|
||||||
}
|
}
|
||||||
|
@ -1162,10 +1150,22 @@ video {
|
||||||
height: 16rem;
|
height: 16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-10 {
|
||||||
|
height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.h-80 {
|
.h-80 {
|
||||||
height: 20rem;
|
height: 20rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h-56 {
|
||||||
|
height: 14rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-20 {
|
||||||
|
height: 5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.max-h-12 {
|
.max-h-12 {
|
||||||
max-height: 3rem;
|
max-height: 3rem;
|
||||||
}
|
}
|
||||||
|
@ -1178,10 +1178,6 @@ video {
|
||||||
max-height: 16rem;
|
max-height: 16rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-h-7 {
|
|
||||||
max-height: 1.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.max-h-60 {
|
.max-h-60 {
|
||||||
max-height: 15rem;
|
max-height: 15rem;
|
||||||
}
|
}
|
||||||
|
@ -1228,14 +1224,14 @@ video {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-3 {
|
|
||||||
width: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-7 {
|
.w-7 {
|
||||||
width: 1.75rem;
|
width: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-3 {
|
||||||
|
width: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.w-11 {
|
.w-11 {
|
||||||
width: 2.75rem;
|
width: 2.75rem;
|
||||||
}
|
}
|
||||||
|
@ -1614,11 +1610,6 @@ video {
|
||||||
border-radius: 0.125rem;
|
border-radius: 0.125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rounded-l {
|
|
||||||
border-top-left-radius: 0.25rem;
|
|
||||||
border-bottom-left-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rounded-t {
|
.rounded-t {
|
||||||
border-top-left-radius: 0.25rem;
|
border-top-left-radius: 0.25rem;
|
||||||
border-top-right-radius: 0.25rem;
|
border-top-right-radius: 0.25rem;
|
||||||
|
@ -1634,6 +1625,11 @@ video {
|
||||||
border-bottom-left-radius: 0.25rem;
|
border-bottom-left-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rounded-l {
|
||||||
|
border-top-left-radius: 0.25rem;
|
||||||
|
border-bottom-left-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.rounded-tl {
|
.rounded-tl {
|
||||||
border-top-left-radius: 0.25rem;
|
border-top-left-radius: 0.25rem;
|
||||||
}
|
}
|
||||||
|
@ -1843,6 +1839,11 @@ video {
|
||||||
padding-right: 0.75rem;
|
padding-right: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.py-1 {
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
padding-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.pr-2 {
|
.pr-2 {
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
@ -2317,6 +2318,14 @@ input[type=text] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.debug input, .debug textarea {
|
||||||
|
border: 6px solid red
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug label input, .debug label textarea {
|
||||||
|
border: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
/************************* BIG CATEGORIES ********************************/
|
/************************* BIG CATEGORIES ********************************/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2490,6 +2499,12 @@ button.link:hover {
|
||||||
fill: var(--foreground-color) !important;
|
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:not(.neutral-label) {
|
||||||
/**
|
/**
|
||||||
* Label should _contain_ the input element
|
* Label should _contain_ the input element
|
||||||
|
@ -2924,14 +2939,6 @@ a.link-underline {
|
||||||
margin-right: 1rem;
|
margin-right: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm\:mr-2 {
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:flex {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:h-24 {
|
.sm\:h-24 {
|
||||||
height: 6rem;
|
height: 6rem;
|
||||||
}
|
}
|
||||||
|
@ -2948,14 +2955,6 @@ a.link-underline {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sm\:items-stretch {
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:justify-between {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sm\:border-4 {
|
.sm\:border-4 {
|
||||||
border-width: 4px;
|
border-width: 4px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Store, UIEventSource } from "../UIEventSource"
|
||||||
import { OsmConnection } from "../Osm/OsmConnection"
|
import { OsmConnection } from "../Osm/OsmConnection"
|
||||||
import { Changes } from "../Osm/Changes"
|
import { Changes } from "../Osm/Changes"
|
||||||
import Translations from "../../UI/i18n/Translations"
|
import Translations from "../../UI/i18n/Translations"
|
||||||
import NoteCommentElement from "../../UI/Popup/NoteCommentElement"
|
import NoteCommentElement from "../../UI/Popup/Notes/NoteCommentElement"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ImageUploadManager has a
|
* The ImageUploadManager has a
|
||||||
|
|
|
@ -23,7 +23,10 @@ export class LocalStorageSource {
|
||||||
|
|
||||||
static Get(key: string, defaultValue: string = undefined): UIEventSource<string> {
|
static Get(key: string, defaultValue: string = undefined): UIEventSource<string> {
|
||||||
try {
|
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)
|
const source = new UIEventSource<string>(saved ?? defaultValue, "localstorage:" + key)
|
||||||
|
|
||||||
source.addCallback((data) => {
|
source.addCallback((data) => {
|
||||||
|
|
|
@ -647,6 +647,12 @@ export default class TagRenderingConfig {
|
||||||
multiSelectedMapping: boolean[] | undefined,
|
multiSelectedMapping: boolean[] | undefined,
|
||||||
currentProperties: Record<string, string>
|
currentProperties: Record<string, string>
|
||||||
): UploadableTag {
|
): UploadableTag {
|
||||||
|
console.log("Constructing change spec", {
|
||||||
|
freeformValue,
|
||||||
|
singleSelectedMapping,
|
||||||
|
multiSelectedMapping,
|
||||||
|
currentProperties,
|
||||||
|
})
|
||||||
if (typeof freeformValue === "string") {
|
if (typeof freeformValue === "string") {
|
||||||
freeformValue = freeformValue?.trim()
|
freeformValue = freeformValue?.trim()
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
import { XCircleIcon } from "@rgossiaux/svelte-heroicons/solid"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
import { trapFocus } from "trap-focus-svelte"
|
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
|
* The slotted element will be shown on top, with a lower-opacity border
|
||||||
|
@ -35,6 +37,7 @@
|
||||||
<button
|
<button
|
||||||
class="absolute right-10 top-10 h-8 w-8 cursor-pointer rounded-full border-none bg-white p-0"
|
class="absolute right-10 top-10 h-8 w-8 cursor-pointer rounded-full border-none bg-white p-0"
|
||||||
on:click={() => dispatch("close")}
|
on:click={() => dispatch("close")}
|
||||||
|
use:ariaLabel={Translations.t.general.backToMap}
|
||||||
>
|
>
|
||||||
<XCircleIcon />
|
<XCircleIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
return {bearing, distance: distanceToCurrentLocation.data.distance}
|
return {bearing, distance: distanceToCurrentLocation.data.distance}
|
||||||
}, [distanceToCurrentLocation])
|
}, [distanceToCurrentLocation])
|
||||||
let viewportCenterDetails = Translations.DynamicSubstitute(t.viewportCenterDetails, relativeBearing)
|
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)]}
|
return {distance, bearing: t.directionsAbsolute[GeoOperations.bearingToHuman(bearing)]}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
|
@ -52,10 +52,9 @@
|
||||||
if (maxDistanceInMeters) {
|
if (maxDistanceInMeters) {
|
||||||
onDestroy(
|
onDestroy(
|
||||||
mla.location.addCallbackD((newLocation) => {
|
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 c: [number, number] = [initialCoordinate.lon, initialCoordinate.lat]
|
||||||
const d = GeoOperations.distanceBetween(l, c)
|
const d = GeoOperations.distanceBetween(l, c)
|
||||||
console.log("distance is", d, l, c)
|
|
||||||
if (d <= maxDistanceInMeters) {
|
if (d <= maxDistanceInMeters) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,9 @@
|
||||||
export let type: ValidatorType
|
export let type: ValidatorType
|
||||||
export let feedback: UIEventSource<Translation> | undefined = undefined
|
export let feedback: UIEventSource<Translation> | undefined = undefined
|
||||||
export let cls: string = undefined
|
export let cls: string = undefined
|
||||||
export let getCountry: () => string | undefined
|
export let getCountry: () => string | undefined = undefined
|
||||||
export let placeholder: string | Translation | undefined
|
export let placeholder: string | Translation | undefined = undefined
|
||||||
|
export let autofocus: boolean = false
|
||||||
export let unit: Unit = undefined
|
export let unit: Unit = undefined
|
||||||
/**
|
/**
|
||||||
* Valid state, exported to the calling component
|
* Valid state, exported to the calling component
|
||||||
|
@ -57,9 +58,9 @@
|
||||||
validator = Validators.get(type ?? "string")
|
validator = Validators.get(type ?? "string")
|
||||||
|
|
||||||
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
|
_placeholder = placeholder ?? validator?.getPlaceholder() ?? type
|
||||||
if(_value.data?.length > 0){
|
if (_value.data?.length > 0) {
|
||||||
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
|
feedback?.setData(validator?.getFeedback(_value.data, getCountry))
|
||||||
}else{
|
} else {
|
||||||
feedback?.setData(undefined)
|
feedback?.setData(undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +70,7 @@
|
||||||
function setValues() {
|
function setValues() {
|
||||||
// Update the value stores
|
// Update the value stores
|
||||||
const v = _value.data
|
const v = _value.data
|
||||||
if(v === ""){
|
if (v === "") {
|
||||||
value.setData(undefined)
|
value.setData(undefined)
|
||||||
feedback?.setData(undefined)
|
feedback?.setData(undefined)
|
||||||
return
|
return
|
||||||
|
@ -100,7 +101,7 @@
|
||||||
if (_value.data !== fromUpstream && fromUpstream !== "") {
|
if (_value.data !== fromUpstream && fromUpstream !== "") {
|
||||||
_value.setData(fromUpstream)
|
_value.setData(fromUpstream)
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
// Handled by the UnitInput
|
// Handled by the UnitInput
|
||||||
|
@ -114,7 +115,7 @@
|
||||||
Utils.sortedByLevenshteinDistance(
|
Utils.sortedByLevenshteinDistance(
|
||||||
type,
|
type,
|
||||||
Validators.AllValidators.map((v) => v.name),
|
Validators.AllValidators.map((v) => v.name),
|
||||||
(v) => v
|
(v) => v,
|
||||||
)
|
)
|
||||||
.slice(0, 5)
|
.slice(0, 5)
|
||||||
.join(", ")
|
.join(", ")
|
||||||
|
@ -123,37 +124,30 @@
|
||||||
|
|
||||||
const isValid = _value.map((v) => validator?.isValid(v, getCountry) ?? true)
|
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) {
|
if (htmlElem !== undefined) {
|
||||||
htmlElem.onfocus = () => dispatch("selected")
|
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>
|
</script>
|
||||||
|
|
||||||
{#if validator?.textArea}
|
{#if validator?.textArea}
|
||||||
<form on:submit|preventDefault={() => sendSubmit()}>
|
|
||||||
<textarea
|
<textarea
|
||||||
class="w-full"
|
class="w-full"
|
||||||
bind:value={$_value}
|
bind:value={$_value}
|
||||||
inputmode={validator?.inputmode ?? "text"}
|
inputmode={validator?.inputmode ?? "text"}
|
||||||
placeholder={_placeholder}
|
placeholder={_placeholder}
|
||||||
|
bind:this={htmlElem}
|
||||||
/>
|
/>
|
||||||
</form>
|
|
||||||
{:else}
|
{:else}
|
||||||
<form class={twMerge("inline-flex", cls)} on:submit|preventDefault={() => sendSubmit()}>
|
<div class={twMerge("inline-flex", cls)}>
|
||||||
<input
|
<input
|
||||||
bind:this={htmlElem}
|
bind:this={htmlElem}
|
||||||
bind:value={$_value}
|
bind:value={$_value}
|
||||||
|
@ -168,5 +162,5 @@
|
||||||
{#if unit !== undefined}
|
{#if unit !== undefined}
|
||||||
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} {getCountry} />
|
<UnitInput {unit} {selectedUnit} textValue={_value} upstreamValue={value} {getCountry} />
|
||||||
{/if}
|
{/if}
|
||||||
</form>
|
</div>
|
||||||
{/if}
|
{/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 BaseUIElement from "../../BaseUIElement"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../../i18n/Translations"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../../Svg"
|
||||||
import Img from "../Base/Img"
|
import Img from "../../Base/Img"
|
||||||
import { SubtleButton } from "../Base/SubtleButton"
|
import { SubtleButton } from "../../Base/SubtleButton"
|
||||||
import Toggle from "../Input/Toggle"
|
import Toggle from "../../Input/Toggle"
|
||||||
import { LoginToggle } from "./LoginButton"
|
import { LoginToggle } from ".././LoginButton"
|
||||||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
import { SpecialVisualization, SpecialVisualizationState } from "../../SpecialVisualization"
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||||
import Constants from "../../Models/Constants"
|
import Constants from "../../../Models/Constants"
|
||||||
|
|
||||||
export class CloseNoteButton implements SpecialVisualization {
|
export class CloseNoteButton implements SpecialVisualization {
|
||||||
public readonly funcName = "close_note"
|
public readonly funcName = "close_note"
|
|
@ -2,21 +2,23 @@
|
||||||
/**
|
/**
|
||||||
* UIcomponent to create a new note at the given location
|
* UIcomponent to create a new note at the given location
|
||||||
*/
|
*/
|
||||||
import type { SpecialVisualizationState } from "../SpecialVisualization"
|
import type { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
import { UIEventSource } from "../../../Logic/UIEventSource"
|
||||||
import { LocalStorageSource } from "../../Logic/Web/LocalStorageSource"
|
import { LocalStorageSource } from "../../../Logic/Web/LocalStorageSource"
|
||||||
import ValidatedInput from "../InputElement/ValidatedInput.svelte"
|
import ValidatedInput from "../../InputElement/ValidatedInput.svelte"
|
||||||
import SubtleButton from "../Base/SubtleButton.svelte"
|
import SubtleButton from "../../Base/SubtleButton.svelte"
|
||||||
import Tr from "../Base/Tr.svelte"
|
import Tr from "../../Base/Tr.svelte"
|
||||||
import Translations from "../i18n/Translations.js"
|
import Translations from "../../i18n/Translations.js"
|
||||||
import type { Feature, Point } from "geojson"
|
import type { Feature, Point } from "geojson"
|
||||||
import LoginToggle from "../Base/LoginToggle.svelte"
|
import LoginToggle from "../../Base/LoginToggle.svelte"
|
||||||
import FilteredLayer from "../../Models/FilteredLayer"
|
import FilteredLayer from "../../../Models/FilteredLayer"
|
||||||
import NewPointLocationInput from "../BigComponents/NewPointLocationInput.svelte"
|
import NewPointLocationInput from "../../BigComponents/NewPointLocationInput.svelte"
|
||||||
import ToSvelte from "../Base/ToSvelte.svelte"
|
import ToSvelte from "../../Base/ToSvelte.svelte"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../../Svg"
|
||||||
import Layers from "../../assets/svg/Layers.svelte"
|
import Layers from "../../../assets/svg/Layers.svelte"
|
||||||
import AddSmall from "../../assets/svg/AddSmall.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 coordinate: UIEventSource<{ lon: number; lat: number }>
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
|
@ -29,12 +31,14 @@
|
||||||
let hasFilter = notelayer?.hasFilter
|
let hasFilter = notelayer?.hasFilter
|
||||||
let isDisplayed = notelayer?.isDisplayed
|
let isDisplayed = notelayer?.isDisplayed
|
||||||
|
|
||||||
|
let submitted = false
|
||||||
function enableNoteLayer() {
|
function enableNoteLayer() {
|
||||||
state.guistate.closeAll()
|
state.guistate.closeAll()
|
||||||
isDisplayed.setData(true)
|
isDisplayed.setData(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function uploadNote() {
|
async function uploadNote() {
|
||||||
|
submitted = true
|
||||||
let txt = comment.data
|
let txt = comment.data
|
||||||
if (txt === undefined || txt === "") {
|
if (txt === undefined || txt === "") {
|
||||||
return
|
return
|
||||||
|
@ -43,7 +47,7 @@
|
||||||
txt += "\n\n #MapComplete #" + state?.layout?.id
|
txt += "\n\n #MapComplete #" + state?.layout?.id
|
||||||
const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt)
|
const id = await state?.osmConnection?.openNote(loc.lat, loc.lon, txt)
|
||||||
console.log("Created a note, got id", id)
|
console.log("Created a note, got id", id)
|
||||||
const feature = <Feature<Point>>{
|
const feature = <Feature<Point, OsmTags>>{
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
geometry: {
|
geometry: {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
|
@ -73,7 +77,6 @@
|
||||||
comment.setData("")
|
comment.setData("")
|
||||||
created = true
|
created = true
|
||||||
state.selectedElement.setData(feature)
|
state.selectedElement.setData(feature)
|
||||||
state.selectedLayer.setData(state.layerState.filteredLayers.get("note"))
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -81,6 +84,8 @@
|
||||||
<div class="alert">
|
<div class="alert">
|
||||||
This theme does not include the layer 'note'. As a result, no nodes can be created
|
This theme does not include the layer 'note'. As a result, no nodes can be created
|
||||||
</div>
|
</div>
|
||||||
|
{:else if submitted}
|
||||||
|
<Loading/>
|
||||||
{:else if created}
|
{:else if created}
|
||||||
<div class="thanks">
|
<div class="thanks">
|
||||||
<Tr t={Translations.t.notes.isCreated} />
|
<Tr t={Translations.t.notes.isCreated} />
|
||||||
|
@ -104,40 +109,41 @@
|
||||||
</SubtleButton>
|
</SubtleButton>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<form class="border-grey-500 rounded-sm border" on:submit|preventDefault={uploadNote}>
|
||||||
<Tr t={Translations.t.notes.createNoteIntro} />
|
<label class="neutral-label">
|
||||||
<div class="border-grey-500 rounded-sm border">
|
|
||||||
|
<Tr t={Translations.t.notes.createNoteIntro} />
|
||||||
<div class="w-full p-1">
|
<div class="w-full p-1">
|
||||||
<ValidatedInput type="text" value={comment} />
|
<ValidatedInput autofocus={true} type="text" value={comment} />
|
||||||
</div>
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
<div class="h-56 w-full">
|
<div class="h-56 w-full">
|
||||||
<NewPointLocationInput value={coordinate} {state}>
|
<NewPointLocationInput value={coordinate} {state}>
|
||||||
<div class="h-20 w-full pb-10" slot="image">
|
<div class="h-20 w-full pb-10" slot="image">
|
||||||
<ToSvelte construct={Svg.note_svg().SetClass("h-10 w-full")} />
|
<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>
|
</div>
|
||||||
</LoginToggle>
|
</NewPointLocationInput>
|
||||||
|
|
||||||
{#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}
|
|
||||||
</div>
|
</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}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
|
@ -1,26 +1,33 @@
|
||||||
import Combine from "../Base/Combine"
|
import Combine from "../../Base/Combine"
|
||||||
import BaseUIElement from "../BaseUIElement"
|
import BaseUIElement from "../../BaseUIElement"
|
||||||
import Svg from "../../Svg"
|
import Svg from "../../../Svg"
|
||||||
import Link from "../Base/Link"
|
import Link from "../../Base/Link"
|
||||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
import { FixedUiElement } from "../../Base/FixedUiElement"
|
||||||
import Translations from "../i18n/Translations"
|
import Translations from "../../i18n/Translations"
|
||||||
import { Utils } from "../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
import Img from "../Base/Img"
|
import Img from "../../Base/Img"
|
||||||
import { SlideShow } from "../Image/SlideShow"
|
import { SlideShow } from "../../Image/SlideShow"
|
||||||
import { Stores, UIEventSource } from "../../Logic/UIEventSource"
|
import { Stores, UIEventSource } from "../../../Logic/UIEventSource"
|
||||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
|
||||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
import { VariableUiElement } from "../../Base/VariableUIElement"
|
||||||
|
import { SpecialVisualizationState } from "../../SpecialVisualization"
|
||||||
|
|
||||||
export default class NoteCommentElement extends Combine {
|
export default class NoteCommentElement extends Combine {
|
||||||
constructor(comment: {
|
constructor(
|
||||||
date: string
|
comment: {
|
||||||
uid: number
|
date: string
|
||||||
user: string
|
uid: number
|
||||||
user_url: string
|
user: string
|
||||||
action: "closed" | "opened" | "reopened" | "commented"
|
user_url: string
|
||||||
text: string
|
action: "closed" | "opened" | "reopened" | "commented"
|
||||||
html: string
|
text: string
|
||||||
}) {
|
html: string
|
||||||
|
highlighted: boolean
|
||||||
|
},
|
||||||
|
state?: SpecialVisualizationState,
|
||||||
|
index?: number,
|
||||||
|
totalNumberOfComments?: number
|
||||||
|
) {
|
||||||
const t = Translations.t.notes
|
const t = Translations.t.notes
|
||||||
|
|
||||||
let actionIcon: BaseUIElement
|
let actionIcon: BaseUIElement
|
||||||
|
@ -68,7 +75,15 @@ export default class NoteCommentElement extends Combine {
|
||||||
let imagesEl: BaseUIElement = undefined
|
let imagesEl: BaseUIElement = undefined
|
||||||
if (images.length > 0) {
|
if (images.length > 0) {
|
||||||
const imageEls = images.map((i) =>
|
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")
|
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")
|
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(
|
public static addCommentTo(
|
||||||
|
@ -107,6 +132,7 @@ export default class NoteCommentElement extends Combine {
|
||||||
action: "commented",
|
action: "commented",
|
||||||
text: txt,
|
text: txt,
|
||||||
html: html,
|
html: html,
|
||||||
|
highlighted: true,
|
||||||
})
|
})
|
||||||
tags.data["comments"] = JSON.stringify(comments)
|
tags.data["comments"] = JSON.stringify(comments)
|
||||||
tags.ping()
|
tags.ping()
|
|
@ -105,7 +105,9 @@
|
||||||
}
|
}
|
||||||
// TODO this has _to much_ values
|
// TODO this has _to much_ values
|
||||||
freeformInput.setData(unseenFreeformValues.join(";"))
|
freeformInput.setData(unseenFreeformValues.join(";"))
|
||||||
checkedMappings.push(unseenFreeformValues.length > 0)
|
if(checkedMappings.length + 1 < mappings.length ){
|
||||||
|
checkedMappings.push(unseenFreeformValues.length > 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (confg.freeform?.key) {
|
if (confg.freeform?.key) {
|
||||||
|
@ -125,13 +127,31 @@
|
||||||
initialize($tags, config)
|
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 {
|
try {
|
||||||
selectedTags = config?.constructChangeSpecification(
|
selectedTags = config?.constructChangeSpecification(
|
||||||
$freeformInput,
|
$freeformInput,
|
||||||
selectedMapping,
|
selectedMapping,
|
||||||
checkedMappings,
|
checkedMappings,
|
||||||
tags.data
|
tags.data,
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could not calculate changeSpecification:", 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 featureSwitchIsTesting = state?.featureSwitchIsTesting ?? new ImmutableStore(false)
|
||||||
let featureSwitchIsDebugging =
|
let featureSwitchIsDebugging =
|
||||||
|
@ -207,148 +214,151 @@
|
||||||
onDestroy(
|
onDestroy(
|
||||||
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
|
state.osmConnection?.userDetails?.addCallbackAndRun((ud) => {
|
||||||
numberOfCs = ud.csCount
|
numberOfCs = ud.csCount
|
||||||
})
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if question !== undefined}
|
{#if question !== undefined}
|
||||||
<div
|
<form
|
||||||
class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
|
class="interactive border-interactive relative flex flex-col overflow-y-auto px-2"
|
||||||
style="max-height: 75vh"
|
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">
|
<span class="font-bold">
|
||||||
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
|
<SpecialTranslation t={question} {tags} {state} {layer} feature={selectedElement} />
|
||||||
</span>
|
</span>
|
||||||
<slot name="upper-right" />
|
<slot name="upper-right" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if config.questionhint}
|
{#if config.questionhint}
|
||||||
<div class="max-h-60 overflow-y-auto">
|
<div class="max-h-60 overflow-y-auto">
|
||||||
<SpecialTranslation
|
<SpecialTranslation
|
||||||
t={config.questionhint}
|
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}
|
{tags}
|
||||||
|
{feedback}
|
||||||
|
{unit}
|
||||||
{state}
|
{state}
|
||||||
{layer}
|
|
||||||
feature={selectedElement}
|
feature={selectedElement}
|
||||||
|
value={freeformInput}
|
||||||
|
on:submit={onSave}
|
||||||
/>
|
/>
|
||||||
</div>
|
{:else if mappings !== undefined && !config.multiAnswer}
|
||||||
{/if}
|
<!-- Simple radiobuttons as mapping -->
|
||||||
|
<div class="flex flex-col">
|
||||||
{#if config.mappings?.length >= 8}
|
{#each config.mappings as mapping, i (mapping.then)}
|
||||||
<div class="sticky flex w-full" aria-hidden="true">
|
<!-- Even though we have a list of 'mappings' already, we still iterate over the list as to keep the original indices-->
|
||||||
<Search class="h-6 w-6" />
|
<TagRenderingMappingInput
|
||||||
<input
|
{mapping}
|
||||||
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}
|
|
||||||
{tags}
|
{tags}
|
||||||
{feedback}
|
|
||||||
{unit}
|
|
||||||
{state}
|
{state}
|
||||||
feature={selectedElement}
|
{selectedElement}
|
||||||
value={freeformInput}
|
{layer}
|
||||||
on:selected={() => (selectedMapping = config.mappings?.length)}
|
{searchTerm}
|
||||||
on:submit={onSave}
|
mappingIsSelected={selectedMapping === i}
|
||||||
/>
|
>
|
||||||
</label>
|
<input
|
||||||
{/if}
|
type="radio"
|
||||||
</div>
|
bind:group={selectedMapping}
|
||||||
{:else if mappings !== undefined && config.multiAnswer}
|
name={"mappings-radio-" + config.id}
|
||||||
<!-- Multiple answers can be chosen: checkboxes -->
|
value={i}
|
||||||
<div class="flex flex-col">
|
on:keypress={(e) => onInputKeypress(e)}
|
||||||
{#each config.mappings as mapping, i (mapping.then)}
|
/>
|
||||||
<TagRenderingMappingInput
|
</TagRenderingMappingInput>
|
||||||
{mapping}
|
{/each}
|
||||||
{tags}
|
{#if config.freeform?.key}
|
||||||
{state}
|
<label class="flex">
|
||||||
{selectedElement}
|
<input
|
||||||
{layer}
|
type="radio"
|
||||||
{searchTerm}
|
bind:group={selectedMapping}
|
||||||
mappingIsSelected={checkedMappings[i]}
|
name={"mappings-radio-" + config.id}
|
||||||
>
|
value={config.mappings?.length}
|
||||||
<input
|
on:keypress={(e) => onInputKeypress(e)}
|
||||||
type="checkbox"
|
/>
|
||||||
name={"mappings-checkbox-" + config.id + "-" + i}
|
<FreeformInput
|
||||||
bind:checked={checkedMappings[i]}
|
{config}
|
||||||
on:keypress={(e) => onInputKeypress(e)}
|
{tags}
|
||||||
/>
|
{feedback}
|
||||||
</TagRenderingMappingInput>
|
{unit}
|
||||||
{/each}
|
{state}
|
||||||
{#if config.freeform?.key}
|
feature={selectedElement}
|
||||||
<label class="flex">
|
value={freeformInput}
|
||||||
<input
|
on:selected={() => (selectedMapping = config.mappings?.length)}
|
||||||
type="checkbox"
|
on:submit={onSave}
|
||||||
name={"mappings-checkbox-" + config.id + "-" + config.mappings?.length}
|
/>
|
||||||
bind:checked={checkedMappings[config.mappings.length]}
|
</label>
|
||||||
on:keypress={(e) => onInputKeypress(e)}
|
{/if}
|
||||||
/>
|
</div>
|
||||||
<FreeformInput
|
{:else if mappings !== undefined && config.multiAnswer}
|
||||||
{config}
|
<!-- Multiple answers can be chosen: checkboxes -->
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#each config.mappings as mapping, i (mapping.then)}
|
||||||
|
<TagRenderingMappingInput
|
||||||
|
{mapping}
|
||||||
{tags}
|
{tags}
|
||||||
{feedback}
|
|
||||||
{unit}
|
|
||||||
{state}
|
{state}
|
||||||
feature={selectedElement}
|
{selectedElement}
|
||||||
value={freeformInput}
|
{layer}
|
||||||
on:submit={onSave}
|
{searchTerm}
|
||||||
/>
|
mappingIsSelected={checkedMappings[i]}
|
||||||
</label>
|
>
|
||||||
{/if}
|
<input
|
||||||
</div>
|
type="checkbox"
|
||||||
{/if}
|
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}>
|
<LoginToggle {state}>
|
||||||
<Loading slot="loading" />
|
<Loading slot="loading" />
|
||||||
|
@ -391,5 +401,5 @@
|
||||||
{/if}
|
{/if}
|
||||||
<slot name="under-buttons" />
|
<slot name="under-buttons" />
|
||||||
</LoginToggle>
|
</LoginToggle>
|
||||||
</div>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -85,9 +85,12 @@
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if confirmedScore !== undefined}
|
{#if confirmedScore !== undefined}
|
||||||
|
<label class="neutral-label">
|
||||||
<Tr cls="font-bold mt-2" t={t.question_opinion} />
|
<Tr cls="font-bold mt-2" t={t.question_opinion} />
|
||||||
<textarea autofocus bind:value={$opinion} inputmode="text" rows="3" class="mb-1 w-full"
|
<textarea autofocus bind:value={$opinion} inputmode="text" rows="3" class="mb-1 w-full"
|
||||||
use:placeholder={t.reviewPlaceholder}/>
|
use:placeholder={t.reviewPlaceholder}/>
|
||||||
|
</label>
|
||||||
|
|
||||||
<Checkbox selected={isAffiliated}>
|
<Checkbox selected={isAffiliated}>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<Tr t={t.i_am_affiliated} />
|
<Tr t={t.i_am_affiliated} />
|
||||||
|
|
|
@ -57,6 +57,9 @@ export interface SpecialVisualizationState {
|
||||||
readonly selectedElement: UIEventSource<Feature>
|
readonly selectedElement: UIEventSource<Feature>
|
||||||
/**
|
/**
|
||||||
* Works together with 'selectedElement' to indicate what properties should be displayed
|
* Works together with 'selectedElement' to indicate what properties should be displayed
|
||||||
|
* @deprecated
|
||||||
|
*
|
||||||
|
* No need to set this anymore
|
||||||
*/
|
*/
|
||||||
readonly selectedLayer: UIEventSource<LayerConfig>
|
readonly selectedLayer: UIEventSource<LayerConfig>
|
||||||
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
|
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
|
||||||
|
|
|
@ -13,10 +13,10 @@ import { MinimapViz } from "./Popup/MinimapViz"
|
||||||
import { ShareLinkViz } from "./Popup/ShareLinkViz"
|
import { ShareLinkViz } from "./Popup/ShareLinkViz"
|
||||||
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
|
import { UploadToOsmViz } from "./Popup/UploadToOsmViz"
|
||||||
import { MultiApplyViz } from "./Popup/MultiApplyViz"
|
import { MultiApplyViz } from "./Popup/MultiApplyViz"
|
||||||
import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz"
|
import { AddNoteCommentViz } from "./Popup/Notes/AddNoteCommentViz"
|
||||||
import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
|
import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz"
|
||||||
import TagApplyButton from "./Popup/TagApplyButton"
|
import TagApplyButton from "./Popup/TagApplyButton"
|
||||||
import { CloseNoteButton } from "./Popup/CloseNoteButton"
|
import { CloseNoteButton } from "./Popup/Notes/CloseNoteButton"
|
||||||
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
|
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
|
||||||
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
|
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
|
||||||
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
|
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
|
||||||
|
@ -30,7 +30,7 @@ import Translations from "./i18n/Translations"
|
||||||
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
|
import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization"
|
||||||
import { SubtleButton } from "./Base/SubtleButton"
|
import { SubtleButton } from "./Base/SubtleButton"
|
||||||
import Svg from "../Svg"
|
import Svg from "../Svg"
|
||||||
import NoteCommentElement from "./Popup/NoteCommentElement"
|
import NoteCommentElement from "./Popup/Notes/NoteCommentElement"
|
||||||
import { SubstitutedTranslation } from "./SubstitutedTranslation"
|
import { SubstitutedTranslation } from "./SubstitutedTranslation"
|
||||||
import List from "./Base/List"
|
import List from "./Base/List"
|
||||||
import StatisticsPanel from "./BigComponents/StatisticsPanel"
|
import StatisticsPanel from "./BigComponents/StatisticsPanel"
|
||||||
|
@ -42,7 +42,7 @@ import SvelteUIElement from "./Base/SvelteUIElement"
|
||||||
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||||
import { Feature } from "geojson"
|
import { Feature } from "geojson"
|
||||||
import { GeoOperations } from "../Logic/GeoOperations"
|
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 AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte"
|
||||||
import UserProfile from "./BigComponents/UserProfile.svelte"
|
import UserProfile from "./BigComponents/UserProfile.svelte"
|
||||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||||
|
@ -1004,7 +1004,10 @@ export default class SpecialVisualizations {
|
||||||
return new Combine(
|
return new Combine(
|
||||||
comments
|
comments
|
||||||
.filter((c) => c.text !== "")
|
.filter((c) => c.text !== "")
|
||||||
.map((c) => new NoteCommentElement(c))
|
.map(
|
||||||
|
(c, i) =>
|
||||||
|
new NoteCommentElement(c, state, i, comments.length)
|
||||||
|
)
|
||||||
).SetClass("flex flex-col")
|
).SetClass("flex flex-col")
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
|
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
|
||||||
import { BBox } from "../Logic/BBox"
|
import { BBox } from "../Logic/BBox"
|
||||||
import { MapLibreAdaptor } from "./Map/MapLibreAdaptor.js"
|
import { MapLibreAdaptor } from "./Map/MapLibreAdaptor.js"
|
||||||
|
import { QueryParameters } from "../Logic/Web/QueryParameters"
|
||||||
|
|
||||||
export let state: ThemeViewState
|
export let state: ThemeViewState
|
||||||
let layout = state.layout
|
let layout = state.layout
|
||||||
|
@ -138,6 +139,15 @@
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
let previewedImage = state.previewedImage
|
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) {
|
function forwardEventToMap(e: KeyboardEvent) {
|
||||||
const mlmap = state.map.data
|
const mlmap = state.map.data
|
||||||
|
|
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
|
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
|
* Searches a child that can be focused on, by first selecting a 'focusable', then a button, then a link
|
||||||
*
|
*
|
||||||
* Returns the focussed element
|
* Returns the focussed element
|
||||||
* @param el
|
* @param el
|
||||||
*/
|
*/
|
||||||
public static focusOnFocusableChild(el: HTMLElement): undefined {
|
public static focusOnFocusableChild(el: HTMLElement): void {
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,6 +121,16 @@ input[type=text] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.debug input, .debug textarea {
|
||||||
|
border: 6px solid red
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.debug label input, .debug label textarea {
|
||||||
|
border: 1px solid grey;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/************************* BIG CATEGORIES ********************************/
|
/************************* BIG CATEGORIES ********************************/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -302,6 +312,11 @@ button.link:hover {
|
||||||
fill: var(--foreground-color) !important;
|
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:not(.neutral-label) {
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue