A11y: move buttons into fields

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

View file

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

View file

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

View file

@ -0,0 +1,102 @@
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"
public readonly needsUrls = [Constants.osmAuthConfig.url]
public readonly docs =
"Button to close a note. A predifined text can be defined to close the note with. If the note is already closed, will show a small text."
public readonly args = [
{
name: "text",
doc: "Text to show on this button",
required: true,
},
{
name: "icon",
doc: "Icon to show",
defaultValue: "checkmark.svg",
},
{
name: "idkey",
doc: "The property name where the ID of the note to close can be found",
defaultValue: "id",
},
{
name: "comment",
doc: "Text to add onto the note when closing",
},
{
name: "minZoom",
doc: "If set, only show the closenote button if zoomed in enough",
},
{
name: "zoomButton",
doc: "Text to show if not zoomed in enough",
},
]
public constr(
state: SpecialVisualizationState,
tags: UIEventSource<Record<string, string>>,
args: string[]
): BaseUIElement {
const t = Translations.t.notes
const params: {
text: string
icon: string
idkey: string
comment: string
minZoom: string
zoomButton: string
} = <any>Utils.ParseVisArgs(this.args, args)
let icon = Svg.checkmark_svg()
if (params.icon !== "checkmark.svg" && (args[2] ?? "") !== "") {
icon = new Img(args[1])
}
let textToShow = t.closeNote
if ((params.text ?? "") !== "") {
textToShow = Translations.T(args[0])
}
let closeButton: BaseUIElement = new SubtleButton(icon, textToShow)
const isClosed = tags.map((tags) => (tags["closed_at"] ?? "") !== "")
closeButton.onClick(() => {
const id = tags.data[args[2] ?? "id"]
state.osmConnection.closeNote(id, args[3])?.then((_) => {
tags.data["closed_at"] = new Date().toISOString()
tags.ping()
})
})
if ((params.minZoom ?? "") !== "" && !isNaN(Number(params.minZoom))) {
closeButton = new Toggle(
closeButton,
params.zoomButton ?? "",
state.mapProperties.zoom.map((zoom) => zoom >= Number(params.minZoom))
)
}
return new LoginToggle(
new Toggle(
t.isClosed.SetClass("thanks"),
closeButton,
isClosed
),
t.loginToClose,
state
)
}
}

View file

@ -0,0 +1,159 @@
<script lang="ts">
/**
* 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 { 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 type { OsmTags } from "../../../Models/OsmFeature"
import Loading from "../../Base/Loading.svelte"
export let coordinate: UIEventSource<{ lon: number; lat: number }>
export let state: SpecialVisualizationState
let comment: UIEventSource<string> = LocalStorageSource.Get("note-text")
let created = false
let notelayer: FilteredLayer = state.layerState.filteredLayers.get("note")
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
}
const loc = coordinate.data
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, OsmTags>>{
type: "Feature",
geometry: {
type: "Point",
coordinates: [loc.lon, loc.lat],
},
properties: {
id: "" + id.id,
date_created: new Date().toISOString(),
_first_comment: txt,
comments: JSON.stringify([
{
text: txt,
html: txt,
user: state.osmConnection?.userDetails?.data?.name,
uid: state.osmConnection?.userDetails?.data?.uid,
},
]),
},
}
// Normally, the 'Changes' will generate the new element. The 'notes' are an exception to this
state.newFeatures.features.data.push(feature)
state.newFeatures.features.ping()
state.selectedElement?.setData(feature)
if (state.featureProperties.trackFeature) {
state.featureProperties.trackFeature(feature)
}
comment.setData("")
created = true
state.selectedElement.setData(feature)
}
</script>
{#if notelayer === undefined}
<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} />
</div>
{:else}
<h3>
<Tr t={Translations.t.notes.createNoteTitle} />
</h3>
{#if $isDisplayed}
<!-- The layer is displayed, so we can add a note without worrying for duplicates -->
{#if $hasFilter}
<div class="flex flex-col">
<!-- ...but a filter is set ...-->
<div class="alert">
<Tr t={Translations.t.notes.noteLayerHasFilters} />
</div>
<SubtleButton on:click={() => notelayer.disableAllFilters()}>
<Layers class="mr-4 h-8 w-8" />
<Tr slot="message" t={Translations.t.notes.disableAllNoteFilters} />
</SubtleButton>
</div>
{:else}
<form class="border-grey-500 rounded-sm border" on:submit|preventDefault={uploadNote}>
<label class="neutral-label">
<Tr t={Translations.t.notes.createNoteIntro} />
<div class="w-full p-1">
<ValidatedInput autofocus={true} type="text" value={comment} />
</div>
</label>
<div class="h-56 w-full">
<NewPointLocationInput value={coordinate} {state}>
<div class="h-20 w-full pb-10" slot="image">
<ToSvelte construct={Svg.note_svg().SetClass("h-10 w-full")} />
</div>
</NewPointLocationInput>
</div>
<LoginToggle {state}>
<span slot="loading"><!--empty: don't show a loading message--></span>
<div slot="not-logged-in" class="alert">
<Tr t={Translations.t.notes.warnAnonymous} />
</div>
</LoginToggle>
{#if $comment?.length >= 3}
<SubtleButton on:click={uploadNote}>
<AddSmall slot="image" class="mr-4 h-8 w-8" />
<Tr slot="message" t={Translations.t.notes.createNote} />
</SubtleButton>
{:else}
<div class="alert">
<Tr t={Translations.t.notes.textNeeded} />
</div>
{/if}
</form>
{/if}
{:else}
<div class="flex flex-col">
<div class="alert">
<Tr t={Translations.t.notes.noteLayerNotEnabled} />
</div>
<SubtleButton on:click={enableNoteLayer}>
<Layers slot="image" class="mr-4 h-8 w-8" />
<Tr slot="message" t={Translations.t.notes.noteLayerDoEnable} />
</SubtleButton>
</div>
{/if}
{/if}

View file

@ -0,0 +1,140 @@
import Combine from "../../Base/Combine"
import BaseUIElement from "../../BaseUIElement"
import Svg from "../../../Svg"
import Link from "../../Base/Link"
import { FixedUiElement } from "../../Base/FixedUiElement"
import Translations from "../../i18n/Translations"
import { Utils } from "../../../Utils"
import Img from "../../Base/Img"
import { SlideShow } from "../../Image/SlideShow"
import { Stores, UIEventSource } from "../../../Logic/UIEventSource"
import { OsmConnection } from "../../../Logic/Osm/OsmConnection"
import { VariableUiElement } from "../../Base/VariableUIElement"
import { SpecialVisualizationState } from "../../SpecialVisualization"
export default class NoteCommentElement extends Combine {
constructor(
comment: {
date: string
uid: number
user: string
user_url: string
action: "closed" | "opened" | "reopened" | "commented"
text: string
html: string
highlighted: boolean
},
state?: SpecialVisualizationState,
index?: number,
totalNumberOfComments?: number
) {
const t = Translations.t.notes
let actionIcon: BaseUIElement
if (comment.action === "opened" || comment.action === "reopened") {
actionIcon = Svg.note_svg()
} else if (comment.action === "closed") {
actionIcon = Svg.resolved_svg()
} else {
actionIcon = Svg.speech_bubble_svg()
}
let user: BaseUIElement
if (comment.user === undefined) {
user = t.anonymous
} else {
user = new Link(comment.user, comment.user_url ?? "", true)
}
let userinfo = Stores.FromPromise(
Utils.downloadJsonCached(
"https://api.openstreetmap.org/api/0.6/user/" + comment.uid,
24 * 60 * 60 * 1000
)
)
let userImg = new VariableUiElement(
userinfo.map((userinfo) => {
const href = userinfo?.user?.img?.href
if (href !== undefined) {
return new Img(href).SetClass("rounded-full w-8 h-8 mr-4")
}
return undefined
})
)
const htmlElement = document.createElement("div")
htmlElement.innerHTML = Utils.purify(comment.html)
const images = Array.from(htmlElement.getElementsByTagName("a"))
.map((link) => link.href)
.filter((link) => {
link = link.toLowerCase()
const lastDotIndex = link.lastIndexOf(".")
const extension = link.substring(lastDotIndex + 1, link.length)
return Utils.imageExtensions.has(extension)
})
let imagesEl: BaseUIElement = undefined
if (images.length > 0) {
const imageEls = images.map((i) =>
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")
}
super([
new Combine([
actionIcon.SetClass("mr-4 w-6").SetStyle("flex-shrink: 0"),
new FixedUiElement(comment.html).SetClass("flex flex-col").SetStyle("margin: 0"),
]).SetClass("flex"),
imagesEl,
new Combine([userImg, user.SetClass("mr-2"), comment.date]).SetClass(
"flex justify-end items-center subtle"
),
])
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(
txt: string,
tags: UIEventSource<any>,
state: { osmConnection: OsmConnection }
) {
const comments: any[] = JSON.parse(tags.data["comments"])
const username = state.osmConnection.userDetails.data.name
var urlRegex = /(https?:\/\/[^\s]+)/g
const html = txt.replace(urlRegex, function (url) {
return '<a href="' + url + '">' + url + "</a>"
})
comments.push({
date: new Date().toISOString(),
uid: state.osmConnection.userDetails.data.uid,
user: username,
user_url: "https://www.openstreetmap.org/user/" + username,
action: "commented",
text: txt,
html: html,
highlighted: true,
})
tags.data["comments"] = JSON.stringify(comments)
tags.ping()
}
}