forked from MapComplete/MapComplete
Add a wikidata search box
This commit is contained in:
parent
54bc4f24da
commit
b5a2ee1757
21 changed files with 4141 additions and 3590 deletions
|
@ -30,10 +30,15 @@ export default class Combine extends BaseUIElement {
|
|||
if (subEl === undefined || subEl === null) {
|
||||
continue;
|
||||
}
|
||||
try{
|
||||
|
||||
const subHtml = subEl.ConstructElement()
|
||||
if (subHtml !== undefined) {
|
||||
el.appendChild(subHtml)
|
||||
}
|
||||
}catch(e){
|
||||
console.error("Could not generate subelement in combine due to ", e)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const domExc = e as DOMException
|
||||
|
|
|
@ -12,10 +12,11 @@ import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader";
|
|||
import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI";
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
|
||||
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image") {
|
||||
constructor(tagsSource: UIEventSource<any>, imagePrefix: string = "image", text: string = undefined) {
|
||||
const uploader = new ImgurUploader(url => {
|
||||
// A file was uploaded - we add it to the tags of the object
|
||||
|
||||
|
@ -43,10 +44,17 @@ export class ImageUploadFlow extends Toggle {
|
|||
const licensePicker = new LicensePicker()
|
||||
|
||||
const t = Translations.t.image;
|
||||
|
||||
let labelContent : BaseUIElement
|
||||
if(text === undefined) {
|
||||
labelContent = Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3 text-4xl ")
|
||||
}else{
|
||||
labelContent = new FixedUiElement(text).SetClass("block align-middle mt-1 ml-3 text-2xl ")
|
||||
}
|
||||
const label = new Combine([
|
||||
Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1"),
|
||||
Translations.t.image.addPicture.Clone().SetClass("block align-middle mt-1 ml-3")
|
||||
]).SetClass("p-2 border-4 border-black rounded-full text-4xl font-bold h-full align-middle w-full flex justify-center")
|
||||
Svg.camera_plus_ui().SetClass("block w-12 h-12 p-1 text-4xl "),
|
||||
labelContent
|
||||
]).SetClass("p-2 border-4 border-black rounded-full font-bold h-full align-middle w-full flex justify-center")
|
||||
|
||||
const fileSelector = new FileSelectorButton(label)
|
||||
fileSelector.GetValue().addCallback(filelist => {
|
||||
|
|
|
@ -16,6 +16,7 @@ import LengthInput from "./LengthInput";
|
|||
import {GeoOperations} from "../../Logic/GeoOperations";
|
||||
import {Unit} from "../../Models/Unit";
|
||||
import {FixedInputElement} from "./FixedInputElement";
|
||||
import WikidataSearchBox from "../Wikipedia/WikidataSearchBox";
|
||||
|
||||
interface TextFieldDef {
|
||||
name: string,
|
||||
|
@ -147,7 +148,7 @@ export default class ValidatedTextField {
|
|||
),
|
||||
ValidatedTextField.tp(
|
||||
"wikidata",
|
||||
"A wikidata identifier, e.g. Q42",
|
||||
"A wikidata identifier, e.g. Q42. Input helper arguments: [ key: the value of this tag will initialize search (default: name), options: { removePrefixes: string[], removePostfixes: string[] } these prefixes and postfixes will be removed from the initial search value]",
|
||||
(str) => {
|
||||
if (str === undefined) {
|
||||
return false;
|
||||
|
@ -163,7 +164,40 @@ export default class ValidatedTextField {
|
|||
str = str.substr(wd.length)
|
||||
}
|
||||
return str.toUpperCase();
|
||||
}),
|
||||
},
|
||||
(currentValue, inputHelperOptions) => {
|
||||
const args = inputHelperOptions.args
|
||||
const searchKey = args[0] ?? "name"
|
||||
|
||||
let searchFor = <string>inputHelperOptions.feature?.properties[searchKey]?.toLowerCase()
|
||||
|
||||
const options = args[1]
|
||||
if (searchFor !== undefined && options !== undefined) {
|
||||
const prefixes = <string[]>options["removePrefixes"]
|
||||
const postfixes = <string[]>options["removePostfixes"]
|
||||
|
||||
for (const postfix of postfixes ?? []) {
|
||||
if (searchFor.endsWith(postfix)) {
|
||||
searchFor = searchFor.substring(0, searchFor.length - postfix.length)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const prefix of prefixes ?? []) {
|
||||
if (searchFor.startsWith(prefix)) {
|
||||
searchFor = searchFor.substring(prefix.length)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return new WikidataSearchBox({
|
||||
value: currentValue,
|
||||
searchText: new UIEventSource<string>(searchFor)
|
||||
})
|
||||
}
|
||||
),
|
||||
|
||||
ValidatedTextField.tp(
|
||||
"int",
|
||||
|
@ -361,13 +395,13 @@ export default class ValidatedTextField {
|
|||
// This implies:
|
||||
// We have to create a dropdown with applicable denominations, and fuse those values
|
||||
const unit = options.unit
|
||||
|
||||
|
||||
|
||||
|
||||
const isSingular = input.GetValue().map(str => str?.trim() === "1")
|
||||
|
||||
const unitDropDown =
|
||||
unit.denominations.length === 1 ?
|
||||
new FixedInputElement( unit.denominations[0].getToggledHuman(isSingular), unit.denominations[0])
|
||||
new FixedInputElement(unit.denominations[0].getToggledHuman(isSingular), unit.denominations[0])
|
||||
: new DropDown("",
|
||||
unit.denominations.map(denom => {
|
||||
return {
|
||||
|
@ -378,17 +412,17 @@ export default class ValidatedTextField {
|
|||
)
|
||||
unitDropDown.GetValue().setData(unit.defaultDenom)
|
||||
unitDropDown.SetClass("w-min")
|
||||
|
||||
const fixedDenom = unit.denominations.length === 1 ? unit.denominations[0] : undefined
|
||||
|
||||
const fixedDenom = unit.denominations.length === 1 ? unit.denominations[0] : undefined
|
||||
input = new CombinedInputElement(
|
||||
input,
|
||||
unitDropDown,
|
||||
// combine the value from the textfield and the dropdown into the resulting value that should go into OSM
|
||||
(text, denom) => {
|
||||
if(denom === undefined){
|
||||
if (denom === undefined) {
|
||||
return text
|
||||
}
|
||||
return denom?.canonicalValue(text, true)
|
||||
return denom?.canonicalValue(text, true)
|
||||
},
|
||||
(valueWithDenom: string) => {
|
||||
// Take the value from OSM and feed it into the textfield and the dropdown
|
||||
|
|
|
@ -26,7 +26,7 @@ import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSou
|
|||
import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer";
|
||||
import Minimap from "./Base/Minimap";
|
||||
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders";
|
||||
import WikipediaBox from "./WikipediaBox";
|
||||
import WikipediaBox from "./Wikipedia/WikipediaBox";
|
||||
|
||||
export interface SpecialVisualization {
|
||||
funcName: string,
|
||||
|
@ -83,9 +83,13 @@ export default class SpecialVisualizations {
|
|||
name: "image-key",
|
||||
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
|
||||
defaultValue: "image"
|
||||
},{
|
||||
name:"label",
|
||||
doc:"The text to show on the button",
|
||||
defaultValue: "Add image"
|
||||
}],
|
||||
constr: (state: State, tags, args) => {
|
||||
return new ImageUploadFlow(tags, args[0])
|
||||
return new ImageUploadFlow(tags, args[0], args[1])
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
80
UI/Wikipedia/WikidataPreviewBox.ts
Normal file
80
UI/Wikipedia/WikidataPreviewBox.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Loading from "../Base/Loading";
|
||||
import {Transform} from "stream";
|
||||
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 Svg from "../../Svg";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
|
||||
export default class WikidataPreviewBox extends VariableUiElement {
|
||||
|
||||
constructor(wikidataId : UIEventSource<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 undefined;
|
||||
}
|
||||
|
||||
if(maybeWikidata === undefined){
|
||||
return new Loading(Translations.t.general.loading)
|
||||
}
|
||||
|
||||
if (maybeWikidata["error"] !== undefined) {
|
||||
return new FixedUiElement(maybeWikidata["error"]).SetClass("alert")
|
||||
}
|
||||
const wikidata = <WikidataResponse> maybeWikidata["success"]
|
||||
return WikidataPreviewBox.WikidataResponsePreview(wikidata)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
public static WikidataResponsePreview(wikidata: WikidataResponse): BaseUIElement{
|
||||
let link = new Link(
|
||||
new Combine([
|
||||
wikidata.id,
|
||||
Svg.wikidata_ui().SetStyle("width: 2.5rem").SetClass("block")
|
||||
]).SetClass("flex"),
|
||||
"https://wikidata.org/wiki/"+wikidata.id ,true)
|
||||
|
||||
let info = new Combine( [
|
||||
new Combine([Translation.fromMap(wikidata.labels).SetClass("font-bold"),
|
||||
link]).SetClass("flex justify-between"),
|
||||
Translation.fromMap(wikidata.descriptions)
|
||||
]).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){
|
||||
imageUrl = WikimediaImageProvider.singleton.PrepUrl(imageUrl).url
|
||||
info = new Combine([ new Img(imageUrl).SetStyle("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
|
||||
}
|
||||
|
||||
}
|
119
UI/Wikipedia/WikidataSearchBox.ts
Normal file
119
UI/Wikipedia/WikidataSearchBox.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
import Combine from "../Base/Combine";
|
||||
import {InputElement} from "../Input/InputElement";
|
||||
import {TextField} from "../Input/TextField";
|
||||
import Translations from "../i18n/Translations";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
|
||||
import Locale from "../i18n/Locale";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import WikidataPreviewBox from "./WikidataPreviewBox";
|
||||
import Title from "../Base/Title";
|
||||
import WikipediaBox from "./WikipediaBox";
|
||||
import Svg from "../../Svg";
|
||||
import Link from "../Base/Link";
|
||||
|
||||
export default class WikidataSearchBox extends InputElement<string> {
|
||||
|
||||
private readonly wikidataId: UIEventSource<string>
|
||||
private readonly searchText: UIEventSource<string>
|
||||
|
||||
constructor(options?: {
|
||||
searchText?: UIEventSource<string>,
|
||||
value?: UIEventSource<string>
|
||||
}) {
|
||||
super();
|
||||
this.searchText = options?.searchText
|
||||
this.wikidataId = options?.value ?? new UIEventSource<string>(undefined);
|
||||
}
|
||||
|
||||
GetValue(): UIEventSource<string> {
|
||||
return this.wikidataId;
|
||||
}
|
||||
|
||||
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 lastSearchResults = new UIEventSource<WikidataResponse[]>([])
|
||||
const searchFailMessage = new UIEventSource(undefined)
|
||||
searchField.GetValue().addCallbackAndRunD(searchText => {
|
||||
if (searchText.length < 3) {
|
||||
return;
|
||||
}
|
||||
searchFailMessage.setData(undefined)
|
||||
lastSearchResults.WaitForPromise(
|
||||
Wikidata.searchAndFetch(searchText, {
|
||||
lang: Locale.language.data,
|
||||
maxCount: 5
|
||||
}
|
||||
), err => searchFailMessage.setData(err))
|
||||
|
||||
})
|
||||
|
||||
|
||||
const previews = new VariableUiElement(lastSearchResults.map(searchResults => {
|
||||
if (searchFailMessage.data !== undefined) {
|
||||
return new Combine([Translations.t.general.wikipedia.failed.Clone().SetClass("alert"), searchFailMessage.data])
|
||||
}
|
||||
|
||||
if(searchResults.length === 0){
|
||||
return Translations.t.general.wikipedia.noResults.Subs({search: searchField.GetValue().data ?? ""})
|
||||
}
|
||||
|
||||
if (searchResults.length === 0) {
|
||||
return Translations.t.general.wikipedia.doSearch
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
}, [searchFailMessage]))
|
||||
|
||||
//
|
||||
const full = new Combine([
|
||||
new Title(Translations.t.general.wikipedia.searchWikidata, 3).SetClass("m-2"),
|
||||
new Combine([
|
||||
Svg.search_ui().SetStyle("width: 1.5rem"),
|
||||
searchField.SetClass("m-2 w-full")]).SetClass("flex"),
|
||||
previews
|
||||
]).SetClass("flex flex-col border-2 border-black rounded-xl m-2 p-2")
|
||||
|
||||
return new Combine([
|
||||
new VariableUiElement(selectedWikidataId.map(wid => {
|
||||
if (wid === undefined) {
|
||||
return undefined
|
||||
}
|
||||
return new WikipediaBox([wid]);
|
||||
})).SetStyle("max-height:12.5rem"),
|
||||
full
|
||||
]).ConstructElement();
|
||||
}
|
||||
|
||||
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||
|
||||
IsValid(t: string): boolean {
|
||||
return t.startsWith("Q") && !isNaN(Number(t.substring(1)));
|
||||
}
|
||||
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
import {UIEventSource} from "../Logic/UIEventSource";
|
||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||
import Wikipedia from "../Logic/Web/Wikipedia";
|
||||
import Loading from "./Base/Loading";
|
||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
||||
import Combine from "./Base/Combine";
|
||||
import BaseUIElement from "./BaseUIElement";
|
||||
import Title from "./Base/Title";
|
||||
import Translations from "./i18n/Translations";
|
||||
import Svg from "../Svg";
|
||||
import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata";
|
||||
import Locale from "./i18n/Locale";
|
||||
import Link from "./Base/Link";
|
||||
import {TabbedComponent} from "./Base/TabbedComponent";
|
||||
import {Translation} from "./i18n/Translation";
|
||||
import BaseUIElement from "../BaseUIElement";
|
||||
import Locale from "../i18n/Locale";
|
||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||
import {Translation} from "../i18n/Translation";
|
||||
import Svg from "../../Svg";
|
||||
import Combine from "../Base/Combine";
|
||||
import Title from "../Base/Title";
|
||||
import Wikipedia from "../../Logic/Web/Wikipedia";
|
||||
import Wikidata, {WikidataResponse} from "../../Logic/Web/Wikidata";
|
||||
import {TabbedComponent} from "../Base/TabbedComponent";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import Loading from "../Base/Loading";
|
||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Link from "../Base/Link";
|
||||
import WikidataPreviewBox from "./WikidataPreviewBox";
|
||||
|
||||
export default class WikipediaBox extends Combine {
|
||||
|
||||
|
@ -116,7 +117,7 @@ export default class WikipediaBox extends Combine {
|
|||
if (status[0] == "no page") {
|
||||
const [_, wd] = <[string, WikidataResponse]> status
|
||||
return new Combine([
|
||||
Translation.fromMap(wd.descriptions) ,
|
||||
WikidataPreviewBox.WikidataResponsePreview(wd),
|
||||
wp.noWikipediaPage.Clone().SetClass("subtle")]).SetClass("flex flex-col p-4")
|
||||
}
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue