Refactoring: dismantle 'inputHelpers'

This commit is contained in:
Pieter Vander Vennet 2025-07-28 03:14:33 +02:00
parent 29dc7d1e03
commit 7c42758b42
26 changed files with 485 additions and 439 deletions

View file

@ -6,25 +6,35 @@
import MaplibreMap from "../../Map/MaplibreMap.svelte"
import Direction_stroke from "../../../assets/svg/Direction_stroke.svelte"
import type { SpecialVisualizationState } from "../../SpecialVisualization"
import type Feature from "geojson"
import { GeoOperations } from "../../../Logic/GeoOperations"
/**
* A visualisation to pick a direction on a map background.
*/
export let value: UIEventSource<undefined | string>
export let state: SpecialVisualizationState = undefined
export let mapProperties: Partial<MapProperties> & {
readonly location: UIEventSource<{ lon: number; lat: number }>
export let args: any[] = []
export let feature: Feature
let [lon, lat] = GeoOperations.centerpointCoordinates(feature)
let mapProperties: MapProperties = {
location: new UIEventSource({ lon, lat }),
zoom: new UIEventSource(args[0] !== undefined ? Number(args[0]) : 17),
rasterLayer: state.mapProperties.rasterLayer,
rotation: state.mapProperties.rotation,
}
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
let mla = new MapLibreAdaptor(map, mapProperties)
mla.allowMoving.setData(false)
mla.allowZooming.setData(false)
state?.mapProperties?.rasterLayer?.addCallbackAndRunD((l) => mla.rasterLayer.set(l))
let rotation = new UIEventSource(value.data)
rotation.addCallbackD(rotation => {
const r = (rotation + mapProperties.rotation.data + 360) % 360
console.log("Setting value to", r)
value.setData(""+Math.floor(r))
}, [mapProperties.rotation])
let directionElem: HTMLElement | undefined
$: value.addCallbackAndRunD((degrees) => {
if (directionElem === undefined) {
$: rotation.addCallbackAndRunD((degrees) => {
if (!directionElem?.style) {
return
}
directionElem.style.rotate = degrees + "deg"
@ -32,13 +42,14 @@
let mainElem: HTMLElement
function onPosChange(x: number, y: number) {
const rect = mainElem.getBoundingClientRect()
const dx = -(rect.left + rect.right) / 2 + x
const dy = (rect.top + rect.bottom) / 2 - y
const angle = (180 * Math.atan2(dy, dx)) / Math.PI
const angleGeo = Math.floor((450 - angle) % 360)
value.setData("" + angleGeo)
rotation.setData(angleGeo)
}
let isDown = false
@ -46,7 +57,7 @@
<div
bind:this={mainElem}
class="relative h-48 w-48 cursor-pointer overflow-hidden"
class="relative h-48 min-w-48 w-full cursor-pointer overflow-hidden rounded-xl"
on:click={(e) => onPosChange(e.x, e.y)}
on:mousedown={(e) => {
isDown = true
@ -71,7 +82,7 @@
<MaplibreMap mapProperties={mla} {map} />
</div>
<div bind:this={directionElem} class="absolute left-0 top-0 h-full w-full">
<div bind:this={directionElem} class="absolute left-0 top-0 h-full w-full p-1">
<Direction_stroke />
</div>
</div>

View file

@ -1,67 +1,25 @@
<script lang="ts">
/**
* Constructs an input helper element for the given type.
* Note that all values are stringified
*/
import { UIEventSource } from "../../Logic/UIEventSource"
import type { ValidatorType } from "./Validators"
import InputHelpers from "./InputHelpers"
import Validators from "./Validators"
import type { Feature } from "geojson"
import ImageHelper from "./Helpers/ImageHelper.svelte"
import TranslationInput from "./Helpers/TranslationInput.svelte"
import TagInput from "./Helpers/TagInput.svelte"
import SimpleTagInput from "./Helpers/SimpleTagInput.svelte"
import DirectionInput from "./Helpers/DirectionInput.svelte"
import DateInput from "./Helpers/DateInput.svelte"
import ColorInput from "./Helpers/ColorInput.svelte"
import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte"
import SlopeInput from "./Helpers/SlopeInput.svelte"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import WikidataInputHelper from "./Helpers/WikidataInputHelper.svelte"
import type { Validator } from "./Validator"
import DistanceInput from "./Helpers/DistanceInput.svelte"
import TimeInput from "./Helpers/TimeInput.svelte"
import CollectionTimes from "./Helpers/CollectionTimes/CollectionTimes.svelte"
export let type: ValidatorType
export let value: UIEventSource<string | object>
export let feature: Feature = undefined
export let args: (string | number | boolean)[] | any = undefined
export let state: SpecialVisualizationState = undefined
let validator = Validators.get(type)
let validatorHelper: Validator = validator.inputHelper
</script>
{#if type === "translation"}
<TranslationInput {value} on:submit {args} />
{:else if type === "direction"}
<DirectionInput
{value}
{state}
mapProperties={InputHelpers.constructMapProperties({ feature, args: args ?? [] })}
/>
{:else if type === "date"}
<DateInput {value} />
{:else if type === "time"}
<TimeInput {value} />
{:else if type === "points_in_time"}
<CollectionTimes {value} />
{:else if type === "color"}
<ColorInput {value} />
{:else if type === "image"}
<ImageHelper {value} />
{:else if type === "tag"}
<TagInput {value} on:submit />
{:else if type === "simple_tag"}
<SimpleTagInput {value} {args} on:submit />
{:else if type === "opening_hours"}
<OpeningHoursInput {value} {args} />
{:else if type === "slope"}
<SlopeInput {value} {feature} {state} />
{:else if type === "wikidata"}
<WikidataInputHelper {value} {feature} {state} {args} />
{:else if type === "distance"}
<DistanceInput {value} {state} {feature} {args} />
{:else}
<slot name="fallback" />
{#if type === "distance"}
<DistanceInput {value} {feature} {state} {args} />
{:else if validatorHelper !== undefined}
<svelte:component this={validatorHelper} {value} {feature} {state} {args} on:submit />
{/if}

View file

@ -1,67 +0,0 @@
import { UIEventSource } from "../../Logic/UIEventSource"
import { MapProperties } from "../../Models/MapProperties"
import { Feature } from "geojson"
import { GeoOperations } from "../../Logic/GeoOperations"
import { ValidatorType } from "./Validators"
export interface InputHelperProperties {
/**
* Extra arguments which might be used by the helper component
*/
args?: (string | number | boolean)[]
/**
* Used for map-based helpers, such as 'direction'
*/
mapProperties?: Partial<MapProperties> & {
readonly location: UIEventSource<{ lon: number; lat: number }>
}
/**
* The feature that this question is about
* Used by the wikidata-input to read properties, which in turn is used to read the name to pre-populate the text field.
* Additionally, used for direction input to set the default location if no mapProperties with location are given
*/
feature?: Feature
}
export default class InputHelpers {
public static hideInputField: ValidatorType[] = ["translation", "simple_tag", "tag","time"]
/**
* Constructs a mapProperties-object for the given properties.
* Assumes that the first helper-args contains the desired zoom-level
* Used for the 'direction' input helper
* @param properties
* @private
*/
public static constructMapProperties(
properties: InputHelperProperties
): Partial<MapProperties> {
let location = properties?.mapProperties?.location
if (!location) {
const [lon, lat] = GeoOperations.centerpointCoordinates(properties.feature)
location = new UIEventSource<{ lon: number; lat: number }>({ lon, lat })
}
let mapProperties: Partial<MapProperties> = properties?.mapProperties ?? { location }
if (!mapProperties.location) {
mapProperties = { ...mapProperties, location }
}
let zoom = 17
if (properties?.args?.[0] !== undefined) {
zoom = Number(properties.args[0])
if (isNaN(zoom)) {
throw "Invalid zoom level for argument at 'length'-input"
}
}
if (!mapProperties.zoom) {
mapProperties = { ...mapProperties, zoom: new UIEventSource<number>(zoom) }
}
if (!mapProperties.rasterLayer) {
/* mapProperties = {
...mapProperties, rasterLayer: properties?.mapProperties?.rasterLayer
}*/
}
return mapProperties
}
}

View file

@ -1,6 +1,7 @@
import { Translation } from "../i18n/Translation"
import Translations from "../i18n/Translations"
import { HTMLInputTypeAttribute } from "svelte/elements"
import { ComponentType } from "svelte/types/runtime/internal/dev"
/**
* A 'TextFieldValidator' contains various methods to check and cleanup an entered value or to give feedback.
@ -21,6 +22,8 @@ export abstract class Validator {
public readonly textArea: boolean
public readonly isMeta?: boolean
public readonly inputHelper : ComponentType = undefined
public readonly hideInputField: boolean = false
constructor(
name: string,
@ -80,4 +83,5 @@ export abstract class Validator {
public validateArguments(args: string): undefined | string {
return undefined
}
}

View file

@ -1,105 +1,114 @@
import { Validator } from "./Validator"
import FloatValidator from "./Validators/FloatValidator"
import StringValidator from "./Validators/StringValidator"
import PFloatValidator from "./Validators/PFloatValidator"
import TextValidator from "./Validators/TextValidator"
import DateValidator from "./Validators/DateValidator"
import { TimeValidator } from "./Validators/TimeValidator"
import NatValidator from "./Validators/NatValidator"
import IntValidator from "./Validators/IntValidator"
import DistanceValidator from "./Validators/DistanceValidator"
import PNatValidator from "./Validators/PNatValidator"
import DirectionValidator from "./Validators/DirectionValidator"
import WikidataValidator from "./Validators/WikidataValidator"
import PNatValidator from "./Validators/PNatValidator"
import FloatValidator from "./Validators/FloatValidator"
import PFloatValidator from "./Validators/PFloatValidator"
import EmailValidator from "./Validators/EmailValidator"
import UrlValidator from "./Validators/UrlValidator"
import PhoneValidator from "./Validators/PhoneValidator"
import OpeningHoursValidator from "./Validators/OpeningHoursValidator"
import PhoneValidator from "./Validators/PhoneValidator"
import ColorValidator from "./Validators/ColorValidator"
import SimpleTagValidator from "./Validators/SimpleTagValidator"
import ImageUrlValidator from "./Validators/ImageUrlValidator"
import TagKeyValidator from "./Validators/TagKeyValidator"
import TranslationValidator from "./Validators/TranslationValidator"
import FediverseValidator from "./Validators/FediverseValidator"
import IconValidator from "./Validators/IconValidator"
import TagValidator from "./Validators/TagValidator"
import IdValidator from "./Validators/IdValidator"
import SlopeValidator from "./Validators/SlopeValidator"
import CollectionTimesValidator from "./Validators/CollectionTimesValidator"
import IdValidator from "./Validators/IdValidator"
import FediverseValidator from "./Validators/FediverseValidator"
import SimpleTagValidator from "./Validators/SimpleTagValidator"
import VeloparkValidator from "./Validators/VeloparkValidator"
import NameSuggestionIndexValidator from "./Validators/NameSuggestionIndexValidator"
import CurrencyValidator from "./Validators/CurrencyValidator"
import RegexValidator from "./Validators/RegexValidator"
import { TimeValidator } from "./Validators/TimeValidator"
import CollectionTimesValidator from "./Validators/CollectionTimesValidator"
import CurrencyValidator from "./Validators/CurrencyValidator"
import TagValidator from "./Validators/TagValidator"
import TranslationValidator from "./Validators/TranslationValidator"
import DistanceValidator from "./Validators/DistanceValidator"
export type ValidatorType = (typeof Validators.availableTypes)[number]
export const availableValidators = [
"color",
"currency",
"date",
"time",
"direction",
"distance",
"email",
"fediverse",
"float",
"icon",
"id",
"image",
"int",
"key",
"nat",
"nsi",
"opening_hours",
"pfloat",
"phone",
"pnat",
"points_in_time",
"regex",
"simple_tag",
"slope",
"string",
"tag",
"text",
"translation",
"url",
"velopark",
"wikidata",
] as const
export type ValidatorType = (typeof availableValidators)[number]
export default class Validators {
public static readonly availableTypes = [
"color",
"currency",
"date",
"time",
"direction",
"distance",
"email",
"fediverse",
"float",
"icon",
"id",
"image",
"int",
"key",
"nat",
"nsi",
"opening_hours",
"pfloat",
"phone",
"pnat",
"points_in_time",
"regex",
"simple_tag",
"slope",
"string",
"tag",
"text",
"translation",
"url",
"velopark",
"wikidata",
] as const
public static readonly availableTypes = availableValidators
public static readonly AllValidators: ReadonlyArray<Validator> = [
new StringValidator(),
new TextValidator(),
new DateValidator(),
new TimeValidator(),
new NatValidator(),
new IntValidator(),
new DistanceValidator(),
new DirectionValidator(),
new WikidataValidator(),
new PNatValidator(),
new FloatValidator(),
new PFloatValidator(),
new EmailValidator(),
new UrlValidator(),
new PhoneValidator(),
new OpeningHoursValidator(),
new TextValidator(),
new NatValidator(),
new PNatValidator(),
new IntValidator(),
new DateValidator(),
new TimeValidator(),
new ColorValidator(),
new ImageUrlValidator(),
new SimpleTagValidator(),
new TagValidator(),
new TagKeyValidator(),
new TranslationValidator(),
new IconValidator(),
new FediverseValidator(),
new IdValidator(),
new DirectionValidator(),
new SlopeValidator(),
new VeloparkValidator(),
new NameSuggestionIndexValidator(),
new UrlValidator(),
new EmailValidator(),
new PhoneValidator(),
new FediverseValidator(),
new ImageUrlValidator(),
new OpeningHoursValidator(),
new CollectionTimesValidator(),
new CurrencyValidator(),
new WikidataValidator(),
new TagKeyValidator(),
new IconValidator(),
new VeloparkValidator(),
new IdValidator(),
new RegexValidator(),
new CollectionTimesValidator()
new SimpleTagValidator(),
new TranslationValidator(),
new TagValidator(),
new NameSuggestionIndexValidator(),
new DistanceValidator(),
]
private static _byType = Validators._byTypeConstructor()

View file

@ -1,7 +1,12 @@
import StringValidator from "./StringValidator"
import { ComponentType } from "svelte/types/runtime/internal/dev"
import CollectionTimes from "../Helpers/CollectionTimes/CollectionTimes.svelte"
export default class CollectionTimesValidator extends StringValidator{
public readonly inputHelper: ComponentType = CollectionTimes
constructor() {
super("points_in_time", "'Points in time' are points according to a fixed schedule, e.g. 'every monday at 10:00'. They are typically used for postbox collection times or times of mass at a place of worship")
}
}

View file

@ -1,7 +1,11 @@
import { Validator } from "../Validator"
import ColorInput from "../Helpers/ColorInput.svelte"
export default class ColorValidator extends Validator {
inputHelper = ColorInput
constructor() {
super("color", "Shows a color picker")
}
}

View file

@ -1,6 +1,10 @@
import { Validator } from "../Validator"
import DateInput from "../Helpers/DateInput.svelte"
export default class DateValidator extends Validator {
public readonly inputHelper = DateInput
public readonly hideInputField = true
constructor() {
super("date", "A date with date picker")
}
@ -25,4 +29,5 @@ export default class DateValidator extends Validator {
return [year, month, day].join("-")
}
}

View file

@ -1,6 +1,9 @@
import IntValidator from "./IntValidator"
import DirectionInput from "../Helpers/DirectionInput.svelte"
export default class DirectionValidator extends IntValidator {
public readonly inputHelper = DirectionInput
constructor() {
super(
"direction",

View file

@ -3,6 +3,7 @@ import { Utils } from "../../../Utils"
import { eliCategory } from "../../../Models/RasterLayerProperties"
export default class DistanceValidator extends Validator {
private readonly docs: string = [
"#### Helper-arguments",
"Options are:",
@ -58,4 +59,5 @@ export default class DistanceValidator extends Validator {
}
return undefined
}
}

View file

@ -1,9 +1,11 @@
import UrlValidator from "./UrlValidator"
import { Translation } from "../../i18n/Translation"
import ImageHelper from "../Helpers/ImageHelper.svelte"
export default class ImageUrlValidator extends UrlValidator {
private static readonly allowedExtensions = ["jpg", "jpeg", "svg", "png"]
public readonly isMeta = true
public readonly inputHelper = ImageHelper
constructor() {
super(
@ -37,4 +39,6 @@ export default class ImageUrlValidator extends UrlValidator {
}
return ImageUrlValidator.hasValidExternsion(str)
}
}

View file

@ -1,7 +1,12 @@
import { Validator } from "../Validator"
import MarkdownUtils from "../../../Utils/MarkdownUtils"
import { ComponentType } from "svelte/types/runtime/internal/dev"
import OpeningHoursInput from "../Helpers/OpeningHoursInput.svelte"
export default class OpeningHoursValidator extends Validator {
public readonly inputHelper= OpeningHoursInput
constructor() {
super(
"opening_hours",
@ -39,4 +44,6 @@ export default class OpeningHoursValidator extends Validator {
].join("\n")
)
}
}

View file

@ -2,12 +2,15 @@ import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import TagKeyValidator from "./TagKeyValidator"
import SimpleTagInput from "../Helpers/SimpleTagInput.svelte"
/**
* Checks that the input conforms `key=value`, where `key` and `value` don't have too much weird characters
*/
export default class SimpleTagValidator extends Validator {
private static readonly KeyValidator = new TagKeyValidator()
public readonly inputHelper = SimpleTagInput
public readonly hideInputField = true
public readonly isMeta = true
constructor() {
@ -50,4 +53,6 @@ export default class SimpleTagValidator extends Validator {
isValid(tag: string, _): boolean {
return this.getFeedback(tag, _) === undefined
}
}

View file

@ -1,6 +1,9 @@
import FloatValidator from "./FloatValidator"
import SlopeInput from "../Helpers/SlopeInput.svelte"
export default class SlopeValidator extends FloatValidator {
public readonly inputHelper =SlopeInput
constructor() {
super(
"slope",
@ -40,4 +43,5 @@ export default class SlopeValidator extends FloatValidator {
}
return super.reformat(str) + lastChar
}
}

View file

@ -1,11 +1,14 @@
import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import TagInput from "../Helpers/TagInput.svelte"
/**
* Checks that the input conforms a JSON-encoded tag expression or a simpleTag`key=value`,
*/
export default class TagValidator extends Validator {
public readonly isMeta = true
public readonly inputHelper = TagInput
public readonly hideInputField = true
constructor() {
super("tag", "A simple tag of the format `key=value` OR a tagExpression")
@ -18,4 +21,5 @@ export default class TagValidator extends Validator {
isValid(tag: string, _): boolean {
return this.getFeedback(tag, _) === undefined
}
}

View file

@ -1,11 +1,15 @@
import { Validator } from "../Validator"
import TimeInput from "../Helpers/TimeInput.svelte"
export class TimeValidator extends Validator {
inputmode = "time"
public readonly inputmode = "time"
public readonly inputHelper = TimeInput
public readonly hideInputField = true
constructor() {
super("time", "A time picker")
}
}

View file

@ -1,7 +1,10 @@
import { Validator } from "../Validator"
import TranslationInput from "../Helpers/TranslationInput.svelte"
export default class TranslationValidator extends Validator {
public readonly inputHelper = TranslationInput
public readonly isMeta = true
public readonly hideInputField = true
constructor() {
super("translation", "Makes sure the the string is of format `Record<string, string>` ")
}
@ -14,4 +17,5 @@ export default class TranslationValidator extends Validator {
return false
}
}
}

View file

@ -3,9 +3,11 @@ import { Validator } from "../Validator"
import { Translation } from "../../i18n/Translation"
import Translations from "../../i18n/Translations"
import MarkdownUtils from "../../../Utils/MarkdownUtils"
import WikidataInputHelper from "../Helpers/WikidataInputHelper.svelte"
export default class WikidataValidator extends Validator {
public static readonly _searchCache = new Map<string, Promise<WikidataResponse[]>>()
public readonly inputHelper = WikidataInputHelper
private static docs = [
"#### Helper arguments",
@ -182,4 +184,5 @@ Another example is to search for species and trees:
}
return clipped
}
}