From d5430891bfadceb9689a0e814df91283509082af Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Thu, 11 Jul 2024 16:59:10 +0200 Subject: [PATCH] Refactoring: port wikidata preview boxes and wikidata item picker to Svelte, fix #2019, fix #797 --- assets/layers/etymology/etymology.json | 1 + .../ImageProviders/WikimediaImageProvider.ts | 9 +- src/Logic/Web/Wikidata.ts | 8 +- src/Logic/Web/Wikipedia.ts | 2 +- .../QuestionableTagRenderingConfigJson.ts | 5 + src/Models/ThemeConfig/TagRenderingConfig.ts | 2 + src/UI/Base/SvelteUIElement.ts | 2 +- src/UI/BigComponents/SearchField.svelte | 59 +++++ .../InputElement/Helpers/WikidataInput.svelte | 145 ++++++++++++ src/UI/InputElement/InputHelper.svelte | 10 +- src/UI/InputElement/InputHelpers.ts | 66 ------ .../Validators/WikidataValidator.ts | 136 ++++++++++- .../InputElement/WikidataInputHelper.svelte | 58 +++++ src/UI/PlantNet/SpeciesButton.svelte | 23 +- .../Popup/TagRendering/FreeformInput.svelte | 8 +- src/UI/Wikipedia/WikidataPreviewBox.ts | 194 ++------------- src/UI/Wikipedia/WikidataQuickfacts.svelte | 49 ++++ src/UI/Wikipedia/WikidataSearchBox.ts | 223 ------------------ src/UI/Wikipedia/Wikidatapreview.svelte | 42 ++++ .../WikidatapreviewWithLoading.svelte | 35 +++ src/UI/Wikipedia/WikipediaArticle.svelte | 5 +- src/UI/i18n/Translation.ts | 5 +- 22 files changed, 580 insertions(+), 507 deletions(-) create mode 100644 src/UI/BigComponents/SearchField.svelte create mode 100644 src/UI/InputElement/Helpers/WikidataInput.svelte create mode 100644 src/UI/InputElement/WikidataInputHelper.svelte create mode 100644 src/UI/Wikipedia/WikidataQuickfacts.svelte delete mode 100644 src/UI/Wikipedia/WikidataSearchBox.ts create mode 100644 src/UI/Wikipedia/Wikidatapreview.svelte create mode 100644 src/UI/Wikipedia/WikidatapreviewWithLoading.svelte diff --git a/assets/layers/etymology/etymology.json b/assets/layers/etymology/etymology.json index 1c61593d10..f522c2eddc 100644 --- a/assets/layers/etymology/etymology.json +++ b/assets/layers/etymology/etymology.json @@ -124,6 +124,7 @@ "helperArgs": [ "name", { + "multiple": "yes", "notInstanceOf": [ "Q79007", "Q22698" diff --git a/src/Logic/ImageProviders/WikimediaImageProvider.ts b/src/Logic/ImageProviders/WikimediaImageProvider.ts index 6678c22ce3..97888f6650 100644 --- a/src/Logic/ImageProviders/WikimediaImageProvider.ts +++ b/src/Logic/ImageProviders/WikimediaImageProvider.ts @@ -78,7 +78,14 @@ export class WikimediaImageProvider extends ImageProvider { return new SvelteUIElement(Wikimedia_commons_white).SetStyle("width:2em;height: 2em") } - public PrepUrl(value: string): ProvidedImage { + public PrepUrl(value: NonNullable): ProvidedImage + public PrepUrl(value: undefined): undefined + + public PrepUrl(value: string): ProvidedImage + public PrepUrl(value: string | undefined): ProvidedImage | undefined{ + if(value === undefined){ + return undefined + } value = WikimediaImageProvider.removeCommonsPrefix(value) if (value.startsWith("File:")) { diff --git a/src/Logic/Web/Wikidata.ts b/src/Logic/Web/Wikidata.ts index 7a2f68ddef..b5d3a0a4ce 100644 --- a/src/Logic/Web/Wikidata.ts +++ b/src/Logic/Web/Wikidata.ts @@ -119,6 +119,8 @@ export interface WikidataAdvancedSearchoptions extends WikidataSearchoptions { notInstanceOf?: number[] } +interface SparqlResult {results: { bindings: {item, label, description, num}[] }} + /** * Utility functions around wikidata */ @@ -202,7 +204,7 @@ export default class Wikidata { } ORDER BY ASC(?num) LIMIT ${options?.maxCount ?? 20}` const url = wds.sparqlQuery(sparql) - const result = await Utils.downloadJson(url) + const result = await Utils.downloadJson(url) /*The full uri of the wikidata-item*/ return result.results.bindings.map(({ item, label, description, num }) => ({ @@ -389,7 +391,7 @@ export default class Wikidata { ' SERVICE wikibase:label { bd:serviceParam wikibase:language "[AUTO_LANGUAGE]". }\n' + "}" const url = wds.sparqlQuery(query) - const result = await Utils.downloadJsonCached(url, 24 * 60 * 60 * 1000) + const result = await Utils.downloadJsonCached(url, 24 * 60 * 60 * 1000) return result.results.bindings } @@ -420,7 +422,7 @@ export default class Wikidata { } const url = "https://www.wikidata.org/wiki/Special:EntityData/" + id + ".json" - const entities = (await Utils.downloadJsonCached(url, 10000)).entities + const entities = (await Utils.downloadJsonCached<{entities}>(url, 10000)).entities const firstKey = Array.from(Object.keys(entities))[0] // Roundabout way to fetch the entity; it might have been a redirect const response = entities[firstKey] diff --git a/src/Logic/Web/Wikipedia.ts b/src/Logic/Web/Wikipedia.ts index 81f7bba907..8ad03e7c2d 100644 --- a/src/Logic/Web/Wikipedia.ts +++ b/src/Logic/Web/Wikipedia.ts @@ -215,7 +215,7 @@ export default class Wikipedia { } private async GetArticleUncachedAsync(pageName: string): Promise { - const response = await Utils.downloadJson(this.getDataUrl(pageName)) + const response = await Utils.downloadJson(this.getDataUrl(pageName)) if (response?.parse?.text === undefined) { return undefined } diff --git a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts index e88bd81032..eade83f774 100644 --- a/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts +++ b/src/Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.ts @@ -289,6 +289,11 @@ export interface QuestionableTagRenderingConfigJson extends TagRenderingConfigJs * group: expert */ postfixDistinguished?: string + /** + * Extra arguments to configure the input element + * group: hidden + */ + helperArgs: any } /** diff --git a/src/Models/ThemeConfig/TagRenderingConfig.ts b/src/Models/ThemeConfig/TagRenderingConfig.ts index 903fc5417a..32b9d6d83a 100644 --- a/src/Models/ThemeConfig/TagRenderingConfig.ts +++ b/src/Models/ThemeConfig/TagRenderingConfig.ts @@ -69,6 +69,7 @@ export default class TagRenderingConfig { readonly inline: boolean readonly default?: string readonly postfixDistinguished?: string + readonly args?: any } public readonly multiAnswer: boolean @@ -203,6 +204,7 @@ export default class TagRenderingConfig { inline: json.freeform.inline ?? false, default: json.freeform.default, postfixDistinguished: json.freeform.postfixDistinguished?.trim(), + args: json.freeform.helperArgs } if (json.freeform["extraTags"] !== undefined) { throw `Freeform.extraTags is defined. This should probably be 'freeform.addExtraTag' (at ${context})` diff --git a/src/UI/Base/SvelteUIElement.ts b/src/UI/Base/SvelteUIElement.ts index 2ac773cc21..1afcaad8fa 100644 --- a/src/UI/Base/SvelteUIElement.ts +++ b/src/UI/Base/SvelteUIElement.ts @@ -26,7 +26,7 @@ export default class SvelteUIElement< constructor(svelteElement, props?: Props, events?: Events, slots?: Slots) { super() - this._svelteComponent = svelteElement + this._svelteComponent = svelteElement this._props = props ?? {} this._events = events this._slots = slots diff --git a/src/UI/BigComponents/SearchField.svelte b/src/UI/BigComponents/SearchField.svelte new file mode 100644 index 0000000000..4ddc736def --- /dev/null +++ b/src/UI/BigComponents/SearchField.svelte @@ -0,0 +1,59 @@ + + +
+
{}}> + {#if isRunning} + {Translations.t.general.search.searching} + {:else} +
+ + { + feedback.set(undefined) + return keypr.key === "Enter" ? _performSearch() : undefined + }} + bind:value={$searchValue} + use:placeholder={placeholderText} + use:ariaLabel={Translations.t.general.search.search} + /> +
+ {#if $feedback !== undefined} + + + {/if} + {/if} +
+
diff --git a/src/UI/InputElement/Helpers/WikidataInput.svelte b/src/UI/InputElement/Helpers/WikidataInput.svelte new file mode 100644 index 0000000000..09532257d1 --- /dev/null +++ b/src/UI/InputElement/Helpers/WikidataInput.svelte @@ -0,0 +1,145 @@ + + +

+ +

+ +
+ + + {#if $searchValue.trim().length === 0} + + {:else if $searchValue.trim().length < 3} + + {:else if $searchResult === undefined} +
+ + + +
+ {:else if $searchResult.error !== undefined} +
+ +
+ {:else if $searchResult.success} + {#if $searchResult.success.length === 0} + + {:else} + {#each $searchResult.success as wikidata} + + + {/each} + {/if} + {/if} + + {#each $selectedWithoutSearch as wikidata} + + + {/each} + +
+ diff --git a/src/UI/InputElement/InputHelper.svelte b/src/UI/InputElement/InputHelper.svelte index 72ce861527..ef4f479d8f 100644 --- a/src/UI/InputElement/InputHelper.svelte +++ b/src/UI/InputElement/InputHelper.svelte @@ -19,6 +19,8 @@ import OpeningHoursInput from "./Helpers/OpeningHoursInput.svelte" import SlopeInput from "./Helpers/SlopeInput.svelte" import type { SpecialVisualizationState } from "../SpecialVisualization" + import WikidataInput from "./Helpers/WikidataInput.svelte" + import WikidataInputHelper from "./WikidataInputHelper.svelte" export let type: ValidatorType export let value: UIEventSource @@ -26,17 +28,13 @@ export let feature: Feature export let args: (string | number | boolean)[] = undefined export let state: SpecialVisualizationState - export let helperArgs: (string | number | boolean)[] - export let key: string - export let extraTags: UIEventSource> - let properties = { feature, args: args ?? [] } {#if type === "translation"} {:else if type === "direction"} - + {:else if type === "date"} {:else if type === "color"} @@ -52,5 +50,5 @@ {:else if type === "slope"} {:else if type === "wikidata"} - InputHelpers.constructWikidataHelper(value, properties)} /> + {/if} diff --git a/src/UI/InputElement/InputHelpers.ts b/src/UI/InputElement/InputHelpers.ts index a6f641a357..d79cc387b8 100644 --- a/src/UI/InputElement/InputHelpers.ts +++ b/src/UI/InputElement/InputHelpers.ts @@ -1,10 +1,6 @@ import { UIEventSource } from "../../Logic/UIEventSource" import { MapProperties } from "../../Models/MapProperties" -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" @@ -68,67 +64,5 @@ export default class InputHelpers { return mapProperties } - public static constructWikidataHelper( - value: UIEventSource, - props: InputHelperProperties - ) { - const inputHelperOptions = props - const args = inputHelperOptions.args ?? [] - const searchKey: string = args[0] ?? "name" - const searchFor: string = - searchKey - .split(";") - .map((k) => inputHelperOptions.feature?.properties[k]?.toLowerCase()) - .find((foundValue) => !!foundValue) ?? "" - - let searchForValue: UIEventSource = new UIEventSource(searchFor) - const options: any = args[1] - if (searchFor !== undefined && options !== undefined) { - const prefixes = >options["removePrefixes"] ?? [] - const postfixes = >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, - }) - } } diff --git a/src/UI/InputElement/Validators/WikidataValidator.ts b/src/UI/InputElement/Validators/WikidataValidator.ts index b2fc48c480..d94fe646a9 100644 --- a/src/UI/InputElement/Validators/WikidataValidator.ts +++ b/src/UI/InputElement/Validators/WikidataValidator.ts @@ -1,13 +1,101 @@ import Combine from "../../Base/Combine" -import Wikidata from "../../../Logic/Web/Wikidata" +import Wikidata, { WikidataResponse } from "../../../Logic/Web/Wikidata" import WikidataSearchBox from "../../Wikipedia/WikidataSearchBox" import { Validator } from "../Validator" import { Translation } from "../../i18n/Translation" import Translations from "../../i18n/Translations" +import Title from "../../Base/Title" +import Table from "../../Base/Table" export default class WikidataValidator extends Validator { + public static readonly _searchCache = new Map>() + + public static docs = new Combine([ + new Title("Helper arguments"), + new Table( + ["name", "doc"], + [ + [ + "key", + "the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search", + ], + [ + "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", + ], + ["multiple", + "If 'yes' or 'true', will allow to select multiple values at once"] + ] + ), + ]), + ], + ] + ), + 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] + }] + } +\`\`\` +`, + ]) constructor() { - super("wikidata", new Combine(["A wikidata identifier, e.g. Q42.", WikidataSearchBox.docs])) + super("wikidata", new Combine(["A wikidata identifier, e.g. Q42.", WikidataValidator.docs])) } public isValid(str): boolean { @@ -44,4 +132,48 @@ export default class WikidataValidator extends Validator { } return out } + + /** + * + * @param searchTerm + * @param postfixesToRemove + * @param prefixesToRemove + * @param language + * + * + * WikidataValidator.removePostAndPrefixes("Elf-Julistraat", [], ["straat", "laan"], "nl") // => "Elf-Juli" + * WikidataValidator.removePostAndPrefixes("Elf-Julistraat", [], {"nl":["straat", "laan"], "en": ["street"]}, "nl") // => "Elf-Juli" + * WikidataValidator.removePostAndPrefixes("Elf-Julistraat", [], {"nl":["straat", "laan"], "en": ["street"]}, "en") // => "Elf-Julistraat" + */ + public static removePostAndPrefixes(searchTerm: string, prefixesToRemove: string[] | Record, postfixesToRemove: string[] | Record, language: string): string { + const prefixes = prefixesToRemove + const postfixes = postfixesToRemove + const prefixesUnwrapped: RegExp[] = ( + Array.isArray(prefixes) ? prefixes : prefixes[language] ?? [] + ).map((s) => new RegExp("^" + s, "i")) + + const postfixesUnwrapped: RegExp[] = ( + Array.isArray(postfixes) ? postfixes : postfixes[language] ?? [] + ).map((s) => new RegExp(s + "$", "i")) + + + let clipped = searchTerm.trim() + + for (const postfix of postfixesUnwrapped) { + const match = searchTerm.trim().match(postfix) + if (match !== null) { + clipped = searchTerm.trim().substring(0, searchTerm.trim().length - match[0].length) + break + } + } + + for (const prefix of prefixesUnwrapped) { + const match = searchTerm.trim().match(prefix) + if (match !== null) { + clipped = searchTerm.trim().substring(match[0].length) + break + } + } + return clipped + } } diff --git a/src/UI/InputElement/WikidataInputHelper.svelte b/src/UI/InputElement/WikidataInputHelper.svelte new file mode 100644 index 0000000000..d36fdd06e8 --- /dev/null +++ b/src/UI/InputElement/WikidataInputHelper.svelte @@ -0,0 +1,58 @@ + + + diff --git a/src/UI/PlantNet/SpeciesButton.svelte b/src/UI/PlantNet/SpeciesButton.svelte index 4b714860f4..80e5f430b0 100644 --- a/src/UI/PlantNet/SpeciesButton.svelte +++ b/src/UI/PlantNet/SpeciesButton.svelte @@ -4,14 +4,13 @@ */ import { createEventDispatcher } from "svelte" import type { PlantNetSpeciesMatch } from "../../Logic/Web/PlantNet" - import { UIEventSource } from "../../Logic/UIEventSource" + import { Store, UIEventSource } from "../../Logic/UIEventSource" import Wikidata from "../../Logic/Web/Wikidata" import NextButton from "../Base/NextButton.svelte" import Loading from "../Base/Loading.svelte" - import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox" import Tr from "../Base/Tr.svelte" import Translations from "../i18n/Translations" - import ToSvelte from "../Base/ToSvelte.svelte" + import WikidatapreviewWithLoading from "../Wikipedia/WikidatapreviewWithLoading.svelte" export let species: PlantNetSpeciesMatch let wikidata = UIEventSource.FromPromise( @@ -46,16 +45,12 @@ /> {:else} - - new WikidataPreviewBox(wikidataId, { - imageStyle: "max-width: 8rem; width: unset; height: 8rem", - extraItems: [ - t.matchPercentage - .Subs({ match: Math.round(species.score * 100) }) - .SetClass("thanks w-fit self-center"), - ], - }).SetClass("w-full")} - /> + + +
+ +
+
{/if} diff --git a/src/UI/Popup/TagRendering/FreeformInput.svelte b/src/UI/Popup/TagRendering/FreeformInput.svelte index 829c4b5e2f..e806ceb85e 100644 --- a/src/UI/Popup/TagRendering/FreeformInput.svelte +++ b/src/UI/Popup/TagRendering/FreeformInput.svelte @@ -15,7 +15,6 @@ export let unvalidatedText: UIEventSource = new UIEventSource(value.data) export let config: TagRenderingConfig export let tags: UIEventSource> - export let extraTags: UIEventSource> export let feature: Feature = undefined export let state: SpecialVisualizationState @@ -28,8 +27,6 @@ inline = false inline = config.freeform?.inline } - let helperArgs = config.freeform?.helperArgs - let key = config.freeform?.key const dispatch = createEventDispatcher<{ selected }>() export let feedback: UIEventSource @@ -73,14 +70,11 @@ {/if} diff --git a/src/UI/Wikipedia/WikidataPreviewBox.ts b/src/UI/Wikipedia/WikidataPreviewBox.ts index aa33b69dea..b4b83d690d 100644 --- a/src/UI/Wikipedia/WikidataPreviewBox.ts +++ b/src/UI/Wikipedia/WikidataPreviewBox.ts @@ -1,34 +1,21 @@ -import { VariableUiElement } from "../Base/VariableUIElement" -import { Store } from "../../Logic/UIEventSource" -import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata" -import { Translation, TypedTranslation } from "../i18n/Translation" -import { FixedUiElement } from "../Base/FixedUiElement" -import Loading from "../Base/Loading" +import { TypedTranslation } from "../i18n/Translation" import Translations from "../i18n/Translations" -import Combine from "../Base/Combine" -import Img from "../Base/Img" -import { WikimediaImageProvider } from "../../Logic/ImageProviders/WikimediaImageProvider" -import Link from "../Base/Link" -import BaseUIElement from "../BaseUIElement" -import { Utils } from "../../Utils" -import SvelteUIElement from "../Base/SvelteUIElement" -import { default as Wikidata_icon } from "../../assets/svg/Wikidata.svelte" import Gender_male from "../../assets/svg/Gender_male.svelte" import Gender_female from "../../assets/svg/Gender_female.svelte" import Gender_inter from "../../assets/svg/Gender_inter.svelte" import Gender_trans from "../../assets/svg/Gender_trans.svelte" import Gender_queer from "../../assets/svg/Gender_queer.svelte" -export default class WikidataPreviewBox extends VariableUiElement { + +export default class WikidataPreviewBox { private static isHuman = [{ p: 31 /*is a*/, q: 5 /* human */ }] - // @ts-ignore - private static extraProperties: { + public static extraProperties: { requires?: { p: number; q?: number }[] property: string + textMode?: Map display: | TypedTranslation<{ value }> - | Map BaseUIElement) /*If translation: Subs({value: * }) */> - textMode?: Map + | Map, }[] = [ { requires: WikidataPreviewBox.isHuman, @@ -36,34 +23,28 @@ export default class WikidataPreviewBox extends VariableUiElement { display: new Map([ [ "Q6581097", - () => new SvelteUIElement(Gender_male).SetStyle("width: 1rem; height: auto"), + Gender_male ], [ "Q6581072", - () => new SvelteUIElement(Gender_female).SetStyle("width: 1rem; height: auto"), + Gender_female ], [ "Q1097630", - () => new SvelteUIElement(Gender_inter).SetStyle("width: 1rem; height: auto"), + Gender_inter ], [ "Q1052281", - () => - new SvelteUIElement(Gender_trans).SetStyle( - "width: 1rem; height: auto" - ) /*'transwomen'*/, + Gender_trans /*'transwomen'*/ ], [ "Q2449503", - () => - new SvelteUIElement(Gender_trans).SetStyle( - "width: 1rem; height: auto" - ) /*'transmen'*/, + Gender_trans /*'transmen'*/ ], [ "Q48270", - () => new SvelteUIElement(Gender_queer).SetStyle("width: 1rem; height: auto"), - ], + Gender_queer + ] ]), textMode: new Map([ ["Q6581097", "♂️"], @@ -71,158 +52,19 @@ export default class WikidataPreviewBox extends VariableUiElement { ["Q1097630", "⚥️"], ["Q1052281", "🏳️‍⚧️" /*'transwomen'*/], ["Q2449503", "🏳️‍⚧️" /*'transmen'*/], - ["Q48270", "🏳️‍🌈 ⚧"], - ]), + ["Q48270", "🏳️‍🌈 ⚧"] + ]) }, { property: "P569", requires: WikidataPreviewBox.isHuman, - display: Translations.t.general.wikipedia.previewbox.born, + display: Translations.t.general.wikipedia.previewbox.born }, { property: "P570", requires: WikidataPreviewBox.isHuman, - display: Translations.t.general.wikipedia.previewbox.died, - }, + display: Translations.t.general.wikipedia.previewbox.died + } ] - constructor( - wikidataId: Store, - options?: { - noImages?: boolean - imageStyle?: string - whileLoading?: BaseUIElement | string - extraItems?: (BaseUIElement | string)[] - } - ) { - let inited = false - const wikidata = wikidataId.stabilized(250).bind((id) => { - if (id === undefined || id === "" || id === "Q") { - return null - } - inited = true - return Wikidata.LoadWikidataEntry(id) - }) - - super( - wikidata.map((maybeWikidata) => { - if (maybeWikidata === null || !inited) { - return options?.whileLoading - } - - if (maybeWikidata === undefined) { - return new Loading(Translations.t.general.loading) - } - - if (maybeWikidata["error"] !== undefined) { - return new FixedUiElement(maybeWikidata["error"]).SetClass("alert") - } - const wikidata = maybeWikidata["success"] - console.log(">>>> got wikidata", wikidata) - return WikidataPreviewBox.WikidataResponsePreview(wikidata, options) - }) - ) - } - - public static WikidataResponsePreview( - wikidata: WikidataResponse, - options?: { - noImages?: boolean - imageStyle?: string - extraItems?: (BaseUIElement | string)[] - } - ): BaseUIElement { - console.log(">>> constructing wikidata preview box", wikidata.labels) - - const link = new Link( - new Combine([ - wikidata.id, - options?.noImages - ? wikidata.id - : new SvelteUIElement(Wikidata_icon) - .SetStyle("width: 2.5rem") - .SetClass("block"), - ]).SetClass("flex"), - Wikidata.IdToArticle(wikidata.id), - true - )?.SetClass("must-link") - let info = new Combine([ - new Combine([ - Translation.fromMap(wikidata.labels)?.SetClass("font-bold"), - link, - ]).SetClass("flex justify-between flex-wrap-reverse"), - Translation.fromMap(wikidata.descriptions, true), - WikidataPreviewBox.QuickFacts(wikidata, options), - ...(options?.extraItems ?? []), - ]).SetClass("flex flex-col link-underline") - - let imageUrl = undefined - if (wikidata.claims.get("P18")?.size > 0) { - imageUrl = Array.from(wikidata.claims.get("P18"))[0] - } - if (imageUrl && !options?.noImages) { - imageUrl = WikimediaImageProvider.singleton.PrepUrl(imageUrl).url - info = new Combine([ - new Img(imageUrl) - .SetStyle(options?.imageStyle ?? "max-width: 5rem; width: unset; height: 4rem") - .SetClass("rounded-xl mr-2"), - info.SetClass("w-full"), - ]).SetClass("flex") - } - - info.SetClass("p-2 w-full") - - return info - } - - public static QuickFacts( - wikidata: WikidataResponse, - options?: { noImages?: boolean } - ): BaseUIElement { - const els: BaseUIElement[] = [] - for (const extraProperty of WikidataPreviewBox.extraProperties) { - let hasAllRequirements = true - for (const requirement of extraProperty.requires) { - if (!wikidata.claims?.has("P" + requirement.p)) { - hasAllRequirements = false - break - } - if (!wikidata.claims?.get("P" + requirement.p).has("Q" + requirement.q)) { - hasAllRequirements = false - break - } - } - if (!hasAllRequirements) { - continue - } - - const key = extraProperty.property - const display = - (options?.noImages ? extraProperty.textMode : extraProperty.display) ?? - extraProperty.display - if (wikidata.claims?.get(key) === undefined) { - continue - } - const value: string[] = Array.from(wikidata.claims.get(key)) - - if (display instanceof Translation) { - els.push(display.Subs({ value: value.join(", ") }).SetClass("m-2")) - continue - } - const constructors = Utils.NoNull(value.map((property) => display.get(property))) - const elems = constructors.map((v) => { - if (typeof v === "string") { - return new FixedUiElement(v) - } else { - return v() - } - }) - els.push(new Combine(elems).SetClass("flex m-2")) - } - if (els.length === 0) { - return undefined - } - - return new Combine(els).SetClass("flex") - } } diff --git a/src/UI/Wikipedia/WikidataQuickfacts.svelte b/src/UI/Wikipedia/WikidataQuickfacts.svelte new file mode 100644 index 0000000000..7d4a29dbfd --- /dev/null +++ b/src/UI/Wikipedia/WikidataQuickfacts.svelte @@ -0,0 +1,49 @@ + + +{#if propertiesToRender.length > 0} +
+ {#each propertiesToRender as property} + {#if typeof property.display === "string" } + {property.display} + {:else if property.display instanceof Translation} + + {:else} + + {/if} + {/each} +
+{/if} + diff --git a/src/UI/Wikipedia/WikidataSearchBox.ts b/src/UI/Wikipedia/WikidataSearchBox.ts deleted file mode 100644 index ebbe4c9582..0000000000 --- a/src/UI/Wikipedia/WikidataSearchBox.ts +++ /dev/null @@ -1,223 +0,0 @@ -import Combine from "../Base/Combine" -import { InputElement } from "../Input/InputElement" -import { TextField } from "../Input/TextField" -import Translations from "../i18n/Translations" -import { ImmutableStore, Store, Stores, UIEventSource } from "../../Logic/UIEventSource" -import Wikidata, { WikidataResponse } from "../../Logic/Web/Wikidata" -import Locale from "../i18n/Locale" -import { VariableUiElement } from "../Base/VariableUIElement" -import WikidataPreviewBox from "./WikidataPreviewBox" -import Title from "../Base/Title" -import Svg from "../../Svg" -import Loading from "../Base/Loading" -import Table from "../Base/Table" -import SvelteUIElement from "../Base/SvelteUIElement" -import Search from "../../assets/svg/Search.svelte" - -export default class WikidataSearchBox extends InputElement { - public static docs = new Combine([ - new Title("Helper arguments"), - new Table( - ["name", "doc"], - [ - [ - "key", - "the value of this tag will initialize search (default: name). This can be a ';'-separated list in which case every key will be inspected. The non-null value will be used as search", - ], - [ - "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] - }] - } -\`\`\` -`, - ]) - private static readonly _searchCache = new Map>() - private readonly wikidataId: UIEventSource - private readonly searchText: UIEventSource - private readonly instanceOf?: number[] - private readonly notInstanceOf?: number[] - - constructor(options?: { - searchText?: UIEventSource - value?: UIEventSource - notInstanceOf?: number[] - instanceOf?: number[] - }) { - super() - this.searchText = options?.searchText - this.wikidataId = options?.value ?? new UIEventSource(undefined) - this.instanceOf = options?.instanceOf - this.notInstanceOf = options?.notInstanceOf - } - - GetValue(): UIEventSource { - return this.wikidataId - } - - IsValid(t: string): boolean { - return t.startsWith("Q") && !isNaN(Number(t.substring(1))) - } - - protected InnerConstructElement(): HTMLElement { - const searchField = new TextField({ - placeholder: Translations.t.general.wikipedia.searchWikidata, - value: this.searchText, - inputStyle: "width: calc(100% - 0.5rem); border: 1px solid black", - }) - const selectedWikidataId = this.wikidataId - - const tooShort = new ImmutableStore<{ success: WikidataResponse[] }>({ success: undefined }) - const searchResult: Store<{ success?: WikidataResponse[]; error?: any }> = searchField - .GetValue() - .bind((searchText) => { - if (searchText.length < 3 && !searchText.match(/[qQ][0-9]+/)) { - return tooShort - } - const lang = Locale.language.data - const key = lang + ":" + searchText - let promise = WikidataSearchBox._searchCache.get(key) - if (promise === undefined) { - promise = Wikidata.searchAndFetch(searchText, { - lang, - maxCount: 5, - notInstanceOf: this.notInstanceOf, - instanceOf: this.instanceOf, - }) - WikidataSearchBox._searchCache.set(key, promise) - } - return Stores.FromPromiseWithErr(promise) - }) - - const previews = new VariableUiElement( - searchResult.map( - (searchResultsOrFail) => { - if (searchField.GetValue().data.length === 0) { - return Translations.t.general.wikipedia.doSearch - } - - if (searchField.GetValue().data.length < 3) { - return Translations.t.general.wikipedia.searchToShort - } - - if (searchResultsOrFail === undefined) { - return new Loading(Translations.t.general.loading) - } - - if (searchResultsOrFail.error !== undefined) { - return new Combine([ - Translations.t.general.wikipedia.failed.Clone().SetClass("alert"), - searchResultsOrFail.error, - ]) - } - - const searchResults = searchResultsOrFail.success - if (searchResults.length === 0) { - return Translations.t.general.wikipedia.noResults.Subs({ - search: searchField.GetValue().data ?? "", - }) - } - - return new Combine( - searchResults.map((wikidataresponse) => { - const el = WikidataPreviewBox.WikidataResponsePreview( - wikidataresponse - ).SetClass( - "rounded-xl p-1 sm:p-2 md:p-3 m-px border-2 sm:border-4 transition-colors" - ) - el.onClick(() => { - selectedWikidataId.setData(wikidataresponse.id) - }) - selectedWikidataId.addCallbackAndRunD((selected) => { - if (selected === wikidataresponse.id) { - el.SetClass("subtle-background border-attention") - } else { - el.RemoveClass("subtle-background") - el.RemoveClass("border-attention") - } - }) - return el - }) - ).SetClass("flex flex-col") - }, - [searchField.GetValue()] - ) - ) - - return new Combine([ - new Title(Translations.t.general.wikipedia.searchWikidata, 3).SetClass("m-2"), - new Combine([ - new SvelteUIElement(Search).SetClass("w-6"), - searchField.SetClass("m-2 w-full"), - ]).SetClass("flex"), - previews, - ]) - .SetClass("flex flex-col border-2 border-black rounded-xl m-2 p-2") - .ConstructElement() - } -} diff --git a/src/UI/Wikipedia/Wikidatapreview.svelte b/src/UI/Wikipedia/Wikidatapreview.svelte new file mode 100644 index 0000000000..634f76ebe0 --- /dev/null +++ b/src/UI/Wikipedia/Wikidatapreview.svelte @@ -0,0 +1,42 @@ + + +
+ + {#if imageUrl} + + {/if} + +
+ + + + + + +
+ +
+ + +
+
diff --git a/src/UI/Wikipedia/WikidatapreviewWithLoading.svelte b/src/UI/Wikipedia/WikidatapreviewWithLoading.svelte new file mode 100644 index 0000000000..98129c66a7 --- /dev/null +++ b/src/UI/Wikipedia/WikidatapreviewWithLoading.svelte @@ -0,0 +1,35 @@ + + +{#if $wikidata === undefined} + + + +{:else if $wikidata["error"]} +
+ {$wikidata["error"]} +
+{:else} + + + + +{/if} diff --git a/src/UI/Wikipedia/WikipediaArticle.svelte b/src/UI/Wikipedia/WikipediaArticle.svelte index 4f7371ae2f..48027f8e11 100644 --- a/src/UI/Wikipedia/WikipediaArticle.svelte +++ b/src/UI/Wikipedia/WikipediaArticle.svelte @@ -10,6 +10,7 @@ import Tr from "../Base/Tr.svelte" import Translations from "../i18n/Translations" import Wikipedia from "../../assets/svg/Wikipedia.svelte" + import Wikidatapreview from "./Wikidatapreview.svelte" /** * Shows a wikipedia-article + wikidata preview for the given item @@ -31,9 +32,7 @@ {/if} {#if $wikipediaDetails.wikidata} - WikidataPreviewBox.WikidataResponsePreview($wikipediaDetails.wikidata)} - /> + {/if} {#if $wikipediaDetails.articleUrl} diff --git a/src/UI/i18n/Translation.ts b/src/UI/i18n/Translation.ts index 61949b13ab..38425e87dc 100644 --- a/src/UI/i18n/Translation.ts +++ b/src/UI/i18n/Translation.ts @@ -26,9 +26,7 @@ export class Translation extends BaseUIElement { ) { super() this._strictLanguages = strictLanguages - if (strictLanguages) { - console.log(">>> strict:", translations) - } + if (translations === undefined) { console.error("Translation without content at " + context) throw `Translation without content (${context})` @@ -138,7 +136,6 @@ export class Translation extends BaseUIElement { static fromMap(transl: Map, strictLanguages: boolean = false) { const translations = {} - console.log("Strict:", strictLanguages) let hasTranslation = false transl?.forEach((value, key) => { translations[key] = value