Refactoring: fix most of the custom input elements, support right click/long tap/double click to add a new element

This commit is contained in:
Pieter Vander Vennet 2023-04-16 03:42:26 +02:00
parent b0052d3a36
commit 1123a72c5e
25 changed files with 390 additions and 531 deletions

View file

@ -0,0 +1,12 @@
<script lang="ts">
/**
* Simple wrapper around the HTML-color field.
*/
import { UIEventSource } from "../../../Logic/UIEventSource";
export let value: UIEventSource<undefined | string>;
</script>
<input bind:value={$value} type="color">

View file

@ -0,0 +1,12 @@
<script lang="ts">
/**
* Simple wrapper around the HTML-date field.
*/
import { UIEventSource } from "../../../Logic/UIEventSource";
export let value: UIEventSource<undefined | string>;
</script>
<input bind:value={$value} type="date">

View file

@ -8,9 +8,9 @@
import Svg from "../../../Svg.js";
/**
* A visualisation to pick a direction on a map background
* A visualisation to pick a direction on a map background.
*/
export let value: UIEventSource<undefined | number>;
export let value: UIEventSource<undefined | string>;
export let mapProperties: Partial<MapProperties> & { readonly location: UIEventSource<{ lon: number; lat: number }> };
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined);
let mla = new MapLibreAdaptor(map, mapProperties);
@ -18,7 +18,6 @@
mla.allowZooming.setData(false)
let directionElem: HTMLElement | undefined;
$: value.addCallbackAndRunD(degrees => {
console.log("Degrees are", degrees, directionElem);
if (directionElem === undefined) {
return;
}
@ -32,7 +31,7 @@
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);
value.setData(""+angleGeo);
}
let isDown = false;
@ -61,7 +60,7 @@
<MaplibreMap {map} attribution={false}></MaplibreMap>
</div>
<div bind:this={directionElem} class="absolute w-full h-full top-0 left-0 border border-red-500">
<div bind:this={directionElem} class="absolute w-full h-full top-0 left-0">
<ToSvelte construct={ Svg.direction_stroke_svg}>

View file

@ -3,11 +3,24 @@
* Constructs an input helper element for the given type.
* Note that all values are stringified
*/
import { AvailableInputHelperType } from "./InputHelpers";
import { UIEventSource } from "../../Logic/UIEventSource";
export let type : AvailableInputHelperType
export let value : UIEventSource<string>
import { UIEventSource } from "../../Logic/UIEventSource";
import type { ValidatorType } from "./Validators";
import InputHelpers from "./InputHelpers";
import ToSvelte from "../Base/ToSvelte.svelte";
import type { Feature } from "geojson";
export let type: ValidatorType;
export let value: UIEventSource<string>;
export let feature: Feature;
export let args: (string | number | boolean)[] = undefined;
let properties = { feature, args: args ?? [] };
let construct = InputHelpers.AvailableInputHelpers[type];
</script>
{#if construct !== undefined}
<ToSvelte construct={() => construct(value, properties)} />
{/if}

View file

@ -1,16 +1,151 @@
import { AvailableRasterLayers } from "../../Models/RasterLayers"
import { ValidatorType } from "./Validators"
import { UIEventSource } from "../../Logic/UIEventSource"
import SvelteUIElement from "../Base/SvelteUIElement"
import DirectionInput from "./Helpers/DirectionInput.svelte"
import { MapProperties } from "../../Models/MapProperties"
import DateInput from "./Helpers/DateInput.svelte"
import ColorInput from "./Helpers/ColorInput.svelte"
import BaseUIElement from "../BaseUIElement"
import OpeningHoursInput from "../OpeningHours/OpeningHoursInput"
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox"
import Wikidata from "../../Logic/Web/Wikidata"
import { Utils } from "../../Utils"
import Locale from "../i18n/Locale"
import { Feature } from "geojson"
import { GeoOperations } from "../../Logic/GeoOperations"
export type AvailableInputHelperType = typeof InputHelpers.AvailableInputHelpers[number]
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 readonly AvailableInputHelpers = [] as const
public static readonly AvailableInputHelpers: Readonly<
Partial<
Record<
ValidatorType,
(
value: UIEventSource<string>,
extraProperties?: InputHelperProperties
) => BaseUIElement
>
>
> = {
direction: (value, properties) =>
new SvelteUIElement(DirectionInput, {
value,
mapProperties: InputHelpers.constructMapProperties(properties),
}),
date: (value) => new SvelteUIElement(DateInput, { value }),
color: (value) => new SvelteUIElement(ColorInput, { value }),
opening_hours: (value) => new OpeningHoursInput(value),
wikidata: InputHelpers.constructWikidataHelper,
} as const
/**
* To port
* direction
* opening_hours
* color
* length
* date
* wikidata
* Constructs a mapProperties-object for the given properties.
* Assumes that the first helper-args contains the desired zoom-level
* @param properties
* @private
*/
private 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]) {
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) }
}
return mapProperties
}
private static constructWikidataHelper(
value: UIEventSource<string>,
props: InputHelperProperties
) {
const inputHelperOptions = props
const args = inputHelperOptions.args ?? []
const searchKey = <string>args[0] ?? "name"
const searchFor = <string>(
(inputHelperOptions.feature?.properties[searchKey]?.toLowerCase() ?? "")
)
let searchForValue: UIEventSource<string> = new UIEventSource(searchFor)
const options: any = args[1]
if (searchFor !== undefined && options !== undefined) {
const prefixes = <string[] | Record<string, string[]>>options["removePrefixes"] ?? []
const postfixes = <string[] | Record<string, string[]>>options["removePostfixes"] ?? []
const defaultValueCandidate = Locale.language.map((lg) => {
const prefixesUnrwapped: RegExp[] = (
Array.isArray(prefixes) ? prefixes : prefixes[lg] ?? []
).map((s) => new RegExp("^" + s, "i"))
const postfixesUnwrapped: RegExp[] = (
Array.isArray(postfixes) ? postfixes : postfixes[lg] ?? []
).map((s) => new RegExp(s + "$", "i"))
let clipped = searchFor
for (const postfix of postfixesUnwrapped) {
const match = searchFor.match(postfix)
if (match !== null) {
clipped = searchFor.substring(0, searchFor.length - match[0].length)
break
}
}
for (const prefix of prefixesUnrwapped) {
const match = searchFor.match(prefix)
if (match !== null) {
clipped = searchFor.substring(match[0].length)
break
}
}
return clipped
})
defaultValueCandidate.addCallbackAndRun((clipped) => searchForValue.setData(clipped))
}
let instanceOf: number[] = Utils.NoNull(
(options?.instanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
let notInstanceOf: number[] = Utils.NoNull(
(options?.notInstanceOf ?? []).map((i) => Wikidata.QIdToNumber(i))
)
return new WikidataSearchBox({
value,
searchText: searchForValue,
instanceOf,
notInstanceOf,
})
}
}

View file

@ -5,15 +5,16 @@
import Validators from "./Validators";
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
import { Translation } from "../i18n/Translation";
import { createEventDispatcher } from "svelte";
import { createEventDispatcher, onDestroy } from "svelte";
export let value: UIEventSource<string>;
// Internal state, only copied to 'value' so that no invalid values leak outside
let _value = new UIEventSource(value.data ?? "");
onDestroy(value.addCallbackAndRun(v => _value.setData(v ?? "")));
export let type: ValidatorType;
let validator = Validators.get(type);
export let feedback: UIEventSource<Translation> | undefined = undefined;
_value.addCallbackAndRun(v => {
onDestroy(_value.addCallbackAndRun(v => {
if (validator.isValid(v)) {
feedback?.setData(undefined);
value.setData(v);
@ -21,7 +22,7 @@
}
value.setData(undefined);
feedback?.setData(validator.getFeedback(v));
});
}))
if (validator === undefined) {
throw "Not a valid type for a validator:" + type;
@ -46,7 +47,7 @@
{#if validator.textArea}
<textarea class="w-full" bind:value={$_value} inputmode={validator.inputmode ?? "text"}></textarea>
{:else }
<span class="flex">
<span class="inline-flex">
<input bind:this={htmlElem} bind:value={$_value} inputmode={validator.inputmode ?? "text"}>
{#if !$isValid}
<ExclamationIcon class="h-6 w-6 -ml-6"></ExclamationIcon>

View file

@ -1,11 +1,14 @@
import IntValidator from "./IntValidator"
import { Validator } from "../Validator"
export default class DirectionValidator extends IntValidator {
constructor() {
super(
"direction",
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)"
[
"A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl).",
"### Input helper",
"This element has an input helper showing a map and 'viewport' indicating the direction. By default, this map is zoomed to zoomlevel 17, but this can be changed with the first argument",
].join("\n\n")
)
}

View file

@ -1,6 +1,4 @@
import Combine from "../../Base/Combine"
import Title from "../../Base/Title"
import Table from "../../Base/Table"
import Wikidata from "../../../Logic/Web/Wikidata"
import { UIEventSource } from "../../../Logic/UIEventSource"
import Locale from "../../i18n/Locale"
@ -10,89 +8,7 @@ import { Validator } from "../Validator"
export default class WikidataValidator extends Validator {
constructor() {
super(
"wikidata",
new Combine([
"A wikidata identifier, e.g. Q42.",
new Title("Helper arguments"),
new Table(
["name", "doc"],
[
["key", "the value of this tag will initialize search (default: name)"],
[
"options",
new Combine([
"A JSON-object of type `{ removePrefixes: string[], removePostfixes: string[] }`.",
new Table(
["subarg", "doc"],
[
[
"removePrefixes",
"remove these snippets of text from the start of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes",
],
[
"removePostfixes",
"remove these snippets of text from the end of the passed string to search. This is either a list OR a hash of languages to a list. The individual strings are interpreted as case ignoring regexes.",
],
[
"instanceOf",
"A list of Q-identifier which indicates that the search results _must_ be an entity of this type, e.g. [`Q5`](https://www.wikidata.org/wiki/Q5) for humans",
],
[
"notInstanceof",
"A list of Q-identifiers which indicates that the search results _must not_ be an entity of this type, e.g. [`Q79007`](https://www.wikidata.org/wiki/Q79007) to filter away all streets from the search results",
],
]
),
]),
],
]
),
new Title("Example usage"),
`The following is the 'freeform'-part of a layer config which will trigger a search for the wikidata item corresponding with the name of the selected feature. It will also remove '-street', '-square', ... if found at the end of the name
\`\`\`json
"freeform": {
"key": "name:etymology:wikidata",
"type": "wikidata",
"helperArgs": [
"name",
{
"removePostfixes": {"en": [
"street",
"boulevard",
"path",
"square",
"plaza",
],
"nl": ["straat","plein","pad","weg",laan"],
"fr":["route (de|de la|de l'| de le)"]
},
"#": "Remove streets and parks from the search results:"
"notInstanceOf": ["Q79007","Q22698"]
}
]
}
\`\`\`
Another example is to search for species and trees:
\`\`\`json
"freeform": {
"key": "species:wikidata",
"type": "wikidata",
"helperArgs": [
"species",
{
"instanceOf": [10884, 16521]
}]
}
\`\`\`
`,
])
)
super("wikidata", new Combine(["A wikidata identifier, e.g. Q42.", WikidataSearchBox.docs]))
}
public isValid(str): boolean {