From 307549b59336fb7c404e337515525f72f5bf824c Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 7 Dec 2023 21:56:32 +0100 Subject: [PATCH] Accessibility: port MoveWizard to Svelte as it handles tab cycling correctly --- src/UI/Popup/MoveWizard.svelte | 144 ++++++++++++++++ src/UI/Popup/MoveWizard.ts | 281 -------------------------------- src/UI/Popup/MoveWizardState.ts | 136 ++++++++++++++++ 3 files changed, 280 insertions(+), 281 deletions(-) create mode 100644 src/UI/Popup/MoveWizard.svelte delete mode 100644 src/UI/Popup/MoveWizard.ts create mode 100644 src/UI/Popup/MoveWizardState.ts diff --git a/src/UI/Popup/MoveWizard.svelte b/src/UI/Popup/MoveWizard.svelte new file mode 100644 index 000000000..e6bebf286 --- /dev/null +++ b/src/UI/Popup/MoveWizard.svelte @@ -0,0 +1,144 @@ + +{#if moveWizardState.reasons.length > 0} + + {#if $notAllowed} +
+ +
+ + +
+
+ {:else if currentStep === "start"} + {#if moveWizardState.reasons.length === 1} + + {:else} + + {/if} + {:else if currentStep === "reason"} +
+ + + {#each moveWizardState.reasons as reasonSpec} + + {/each} +
+ + {:else if currentStep === "pick_location"} +
+ + + +
+ +
+ +
+
+ + {#if $reason.includeSearch} + + {/if} + + +
+ zoom >= Constants.minZoomLevelToAddNewPoint)}> + + + +
+ +
+ +
+ + + +
+
+ + + {:else if currentStep === "moved"} + +
+ + +
+ + {/if} +{/if} diff --git a/src/UI/Popup/MoveWizard.ts b/src/UI/Popup/MoveWizard.ts deleted file mode 100644 index 6d1de64eb..000000000 --- a/src/UI/Popup/MoveWizard.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { SubtleButton } from "../Base/SubtleButton" -import Combine from "../Base/Combine" -import Svg from "../../Svg" -import Toggle from "../Input/Toggle" -import { UIEventSource } from "../../Logic/UIEventSource" -import Translations from "../i18n/Translations" -import { VariableUiElement } from "../Base/VariableUIElement" -import { Translation } from "../i18n/Translation" -import BaseUIElement from "../BaseUIElement" -import { GeoOperations } from "../../Logic/GeoOperations" -import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction" -import MoveConfig from "../../Models/ThemeConfig/MoveConfig" -import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" -import { And } from "../../Logic/Tags/And" -import { Tag } from "../../Logic/Tags/Tag" -import { LoginToggle } from "./LoginButton" -import { SpecialVisualizationState } from "../SpecialVisualization" -import { Feature, Point } from "geojson" -import { OsmTags } from "../../Models/OsmFeature" -import SvelteUIElement from "../Base/SvelteUIElement" -import { MapProperties } from "../../Models/MapProperties" -import LocationInput from "../InputElement/Helpers/LocationInput.svelte" -import Geosearch from "../BigComponents/Geosearch.svelte" -import Constants from "../../Models/Constants" -import OpenBackgroundSelectorButton from "../BigComponents/OpenBackgroundSelectorButton.svelte" - -interface MoveReason { - text: Translation | string - invitingText: Translation | string - icon: BaseUIElement - changesetCommentValue: string - lockBounds: true | boolean - includeSearch: false | boolean - background: undefined | "map" | "photo" | string | string[] - startZoom: number - minZoom: number - eraseAddressFields: false | boolean -} - -export default class MoveWizard extends Toggle { - /** - * The UI-element which helps moving a point - */ - constructor( - featureToMove: Feature, - tags: UIEventSource, - state: SpecialVisualizationState, - options: MoveConfig - ) { - const t = Translations.t.move - - const reasons: MoveReason[] = [] - if (options.enableRelocation) { - reasons.push({ - text: t.reasons.reasonRelocation, - invitingText: t.inviteToMove.reasonRelocation, - icon: Svg.relocation_svg(), - changesetCommentValue: "relocated", - lockBounds: false, - background: undefined, - includeSearch: true, - startZoom: 12, - minZoom: 6, - eraseAddressFields: true, - }) - } - if (options.enableImproveAccuracy) { - reasons.push({ - text: t.reasons.reasonInaccurate, - invitingText: t.inviteToMove.reasonInaccurate, - icon: Svg.crosshair_svg(), - changesetCommentValue: "improve_accuracy", - lockBounds: true, - includeSearch: false, - background: "photo", - startZoom: 18, - minZoom: 16, - eraseAddressFields: false, - }) - } - - const currentStep = new UIEventSource<"start" | "reason" | "pick_location" | "moved">( - "start" - ) - const moveReason = new UIEventSource(undefined) - let moveButton: BaseUIElement - if (reasons.length === 1) { - const reason = reasons[0] - moveReason.setData(reason) - moveButton = new SubtleButton( - reason.icon.SetStyle("height: 1.5rem; width: 1.5rem;"), - Translations.T(reason.invitingText) - ).onClick(() => { - currentStep.setData("pick_location") - }) - } else { - moveButton = new SubtleButton( - Svg.move_svg().SetStyle("width: 1.5rem; height: 1.5rem"), - t.inviteToMove.generic - ).onClick(() => { - currentStep.setData("reason") - }) - } - - const moveAgainButton = new SubtleButton(Svg.move_svg(), t.inviteToMoveAgain).onClick( - () => { - currentStep.setData("reason") - } - ) - - const selectReason = new Combine( - reasons.map((r) => - new SubtleButton(r.icon, r.text).onClick(() => { - moveReason.setData(r) - currentStep.setData("pick_location") - }) - ) - ) - - const cancelButton = new SubtleButton(Svg.close_svg(), t.cancel).onClick(() => - currentStep.setData("start") - ) - - const [lon, lat] = GeoOperations.centerpointCoordinates(featureToMove) - const locationInput = moveReason.map((reason) => { - if (reason === undefined) { - return undefined - } - - const mapProperties: Partial = { - minzoom: new UIEventSource(reason.minZoom), - zoom: new UIEventSource(reason?.startZoom ?? 16), - location: new UIEventSource({ lon, lat }), - bounds: new UIEventSource(undefined), - rasterLayer: state.mapProperties.rasterLayer, - } - const value = new UIEventSource<{ lon: number; lat: number }>(undefined) - const locationInput = new Combine([ - new SvelteUIElement(LocationInput, { - mapProperties, - value, - initialCoordinate: { lon, lat }, - }).SetClass("w-full h-full"), - new SvelteUIElement(OpenBackgroundSelectorButton, { state }).SetClass( - "absolute bottom-0 left-0" - ), - ]).SetClass("relative w-full h-full") - - let searchPanel: BaseUIElement = undefined - if (reason.includeSearch) { - searchPanel = new SvelteUIElement(Geosearch, { - bounds: mapProperties.bounds, - clearAfterView: false, - }) - } - - locationInput.SetStyle("height: 17.5rem") - - const confirmMove = new SubtleButton(Svg.move_confirm_svg(), t.confirmMove) - confirmMove.onClick(async () => { - const loc = value.data - await state.changes.applyAction( - new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], { - reason: reason.changesetCommentValue, - theme: state.layout.id, - }) - ) - featureToMove.properties._lat = loc.lat - featureToMove.properties._lon = loc.lon - featureToMove.geometry.coordinates = [loc.lon, loc.lat] - if (reason.eraseAddressFields) { - await state.changes.applyAction( - new ChangeTagAction( - featureToMove.properties.id, - new And([ - new Tag("addr:housenumber", ""), - new Tag("addr:street", ""), - new Tag("addr:city", ""), - new Tag("addr:postcode", ""), - ]), - featureToMove.properties, - { - changeType: "relocated", - theme: state.layout.id, - } - ) - ) - } - - state.featureProperties.getStore(id).ping() - currentStep.setData("moved") - state.mapProperties.location.setData(loc) - }) - const zoomInFurhter = t.zoomInFurther.SetClass("alert block m-6") - return new Combine([ - searchPanel, - locationInput, - new Toggle( - confirmMove, - zoomInFurhter, - mapProperties.zoom.map((zoom) => zoom >= Constants.minZoomLevelToAddNewPoint) - ), - ]).SetClass("flex flex-col") - }) - - const dialogClasses = "p-2 md:p-4 m-2 border border-gray-400 rounded-xl flex flex-col" - - const moveFlow = new LoginToggle( - new VariableUiElement( - currentStep.map((currentStep) => { - switch (currentStep) { - case "start": - return moveButton - case "reason": - return new Combine([ - t.whyMove.SetClass("text-lg font-bold"), - selectReason, - cancelButton, - ]).SetClass(dialogClasses) - case "pick_location": - return new Combine([ - t.moveTitle.SetClass("text-lg font-bold"), - new VariableUiElement(locationInput), - cancelButton, - ]).SetClass(dialogClasses) - case "moved": - return new Combine([ - t.pointIsMoved.SetClass("thanks"), - moveAgainButton, - ]).SetClass("flex flex-col") - } - }) - ), - undefined, - state - ) - let id = featureToMove.properties.id - const backend = state.osmConnection._oauth_config.url - if (id.startsWith(backend)) { - id = id.substring(backend.length) - } - - const moveDisallowedReason = new UIEventSource(undefined) - if (id.startsWith("way")) { - moveDisallowedReason.setData(t.isWay) - } else if (id.startsWith("relation")) { - moveDisallowedReason.setData(t.isRelation) - } else if (id.indexOf("-") < 0) { - state.osmObjectDownloader.DownloadReferencingWays(id).then((referencing) => { - if (referencing.length > 0) { - console.log("Got a referencing way, move not allowed") - moveDisallowedReason.setData(t.partOfAWay) - } - }) - state.osmObjectDownloader.DownloadReferencingRelations(id).then((partOf) => { - if (partOf.length > 0) { - moveDisallowedReason.setData(t.partOfRelation) - } - }) - } - super( - moveFlow, - new Combine([ - Svg.move_not_allowed_svg().SetStyle("height: 2rem").SetClass("m-2"), - new Combine([ - t.cannotBeMoved, - new VariableUiElement(moveDisallowedReason).SetClass("subtle"), - ]).SetClass("flex flex-col"), - ]).SetClass("flex m-2 p-2 rounded-lg bg-gray-200"), - moveDisallowedReason.map((r) => r === undefined) - ) - - const self = this - currentStep.addCallback((state) => { - if (state === "start") { - return - } - self.ScrollIntoView() - }) - } -} diff --git a/src/UI/Popup/MoveWizardState.ts b/src/UI/Popup/MoveWizardState.ts new file mode 100644 index 000000000..5ffdf689b --- /dev/null +++ b/src/UI/Popup/MoveWizardState.ts @@ -0,0 +1,136 @@ +import Svg from "../../Svg" +import { UIEventSource } from "../../Logic/UIEventSource" +import Translations from "../i18n/Translations" +import { Translation } from "../i18n/Translation" +import BaseUIElement from "../BaseUIElement" +import ChangeLocationAction from "../../Logic/Osm/Actions/ChangeLocationAction" +import MoveConfig from "../../Models/ThemeConfig/MoveConfig" +import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" +import { And } from "../../Logic/Tags/And" +import { Tag } from "../../Logic/Tags/Tag" +import { SpecialVisualizationState } from "../SpecialVisualization" +import { Feature, Point } from "geojson" + +export interface MoveReason { + text: Translation | string + invitingText: Translation | string + icon: BaseUIElement + changesetCommentValue: string + lockBounds: true | boolean + includeSearch: false | boolean + background: undefined | "map" | "photo" | string | string[] + startZoom: number + minZoom: number + eraseAddressFields: false | boolean +} + +export class MoveWizardState { + public readonly reasons: ReadonlyArray + + public readonly moveDisallowedReason = new UIEventSource(undefined) + private readonly _state: SpecialVisualizationState + + constructor(id: string, options: MoveConfig, state: SpecialVisualizationState) { + this._state = state + const t = Translations.t.move + + this.reasons = MoveWizardState.initReasons(options) + + if (this.reasons.length > 0) { + this.checkIsAllowed(id) + } + } + + private static initReasons(options: MoveConfig): MoveReason[] { + const t = Translations.t.move + + const reasons: MoveReason[] = [] + if (options.enableRelocation) { + reasons.push({ + text: t.reasons.reasonRelocation, + invitingText: t.inviteToMove.reasonRelocation, + icon: Svg.relocation_svg(), + changesetCommentValue: "relocated", + lockBounds: false, + background: undefined, + includeSearch: true, + startZoom: 12, + minZoom: 6, + eraseAddressFields: true, + }) + } + if (options.enableImproveAccuracy) { + reasons.push({ + text: t.reasons.reasonInaccurate, + invitingText: t.inviteToMove.reasonInaccurate, + icon: Svg.crosshair_svg(), + changesetCommentValue: "improve_accuracy", + lockBounds: true, + includeSearch: false, + background: "photo", + startZoom: 18, + minZoom: 16, + eraseAddressFields: false, + }) + } + return reasons + } + + public async moveFeature( + loc: { lon: number; lat: number }, + reason: MoveReason, + featureToMove: Feature + ) { + const state = this._state + await state.changes.applyAction( + new ChangeLocationAction(featureToMove.properties.id, [loc.lon, loc.lat], { + reason: reason.changesetCommentValue, + theme: state.layout.id, + }) + ) + featureToMove.properties._lat = loc.lat + featureToMove.properties._lon = loc.lon + featureToMove.geometry.coordinates = [loc.lon, loc.lat] + if (reason.eraseAddressFields) { + await state.changes.applyAction( + new ChangeTagAction( + featureToMove.properties.id, + new And([ + new Tag("addr:housenumber", ""), + new Tag("addr:street", ""), + new Tag("addr:city", ""), + new Tag("addr:postcode", ""), + ]), + featureToMove.properties, + { + changeType: "relocated", + theme: state.layout.id, + } + ) + ) + } + + state.featureProperties.getStore(featureToMove.properties.id)?.ping() + state.mapProperties.location.setData(loc) + } + + private checkIsAllowed(id: string) { + const t = Translations.t.move + if (id.startsWith("way")) { + this.moveDisallowedReason.setData(t.isWay) + } else if (id.startsWith("relation")) { + this.moveDisallowedReason.setData(t.isRelation) + } else if (id.indexOf("-") < 0) { + this._state.osmObjectDownloader.DownloadReferencingWays(id).then((referencing) => { + if (referencing.length > 0) { + this.moveDisallowedReason.setData(t.partOfAWay) + } + }) + this._state.osmObjectDownloader.DownloadReferencingRelations(id).then((partOf) => { + if (partOf.length > 0) { + this.moveDisallowedReason.setData(t.partOfRelation) + } + }) + } + } +}