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,12 +109,14 @@ | |||
|         </SubtleButton> | ||||
|       </div> | ||||
|     {:else} | ||||
|       <div> | ||||
|       <form class="border-grey-500 rounded-sm border" on:submit|preventDefault={uploadNote}> | ||||
|         <label class="neutral-label"> | ||||
| 
 | ||||
|           <Tr t={Translations.t.notes.createNoteIntro} /> | ||||
|         <div class="border-grey-500 rounded-sm border"> | ||||
|           <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}> | ||||
|  | @ -136,8 +143,7 @@ | |||
|             <Tr t={Translations.t.notes.textNeeded} /> | ||||
|           </div> | ||||
|         {/if} | ||||
|         </div> | ||||
|       </div> | ||||
|       </form> | ||||
|     {/if} | ||||
|   {:else} | ||||
|     <div class="flex flex-col"> | ||||
|  | @ -1,18 +1,20 @@ | |||
| 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: { | ||||
|     constructor( | ||||
|         comment: { | ||||
|             date: string | ||||
|             uid: number | ||||
|             user: string | ||||
|  | @ -20,7 +22,12 @@ export default class NoteCommentElement extends Combine { | |||
|             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,9 +105,11 @@ | |||
|         } | ||||
|         // TODO this has _to much_ values | ||||
|         freeformInput.setData(unseenFreeformValues.join(";")) | ||||
|         if(checkedMappings.length + 1 < mappings.length ){ | ||||
|           checkedMappings.push(unseenFreeformValues.length > 0) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (confg.freeform?.key) { | ||||
|       if (!confg.multiAnswer) { | ||||
|         // Somehow, setting multi-answer freeform values is broken if this is not set | ||||
|  | @ -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,16 +214,18 @@ | |||
|     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()} | ||||
|   > | ||||
|     <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} /> | ||||
|  | @ -349,6 +358,7 @@ | |||
|           {/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…
	
	Add table
		Add a link
		
	
		Reference in a new issue