forked from MapComplete/MapComplete
Refactoring: port wikipedia panel to Svelte
This commit is contained in:
parent
24f7610d0a
commit
d8e14927c8
32 changed files with 362 additions and 847 deletions
|
@ -1,163 +0,0 @@
|
|||
import Svg from "../../Svg"
|
||||
import Combine from "./Combine"
|
||||
import { FixedUiElement } from "./FixedUiElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Hash from "../../Logic/Web/Hash"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Title from "./Title"
|
||||
import Hotkeys from "./Hotkeys"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
/**
|
||||
*
|
||||
* The scrollableFullScreen is a bit of a peculiar component:
|
||||
* - It shows a title and some contents, constructed from the respective functions passed into the constructor
|
||||
* - When the element is 'activated', one clone of title+contents is attached to the fullscreen
|
||||
* - The element itself will - upon rendering - also show the title and contents (allthough it'll be a different clone)
|
||||
*
|
||||
*
|
||||
*/
|
||||
export default class ScrollableFullScreen {
|
||||
private static readonly empty = ScrollableFullScreen.initEmpty()
|
||||
private static _currentlyOpen: ScrollableFullScreen
|
||||
public isShown: UIEventSource<boolean>
|
||||
private hashToShow: string
|
||||
private _fullscreencomponent: BaseUIElement
|
||||
private _resetScrollSignal: UIEventSource<void> = new UIEventSource<void>(undefined)
|
||||
private _setHash: boolean
|
||||
|
||||
constructor(
|
||||
title: (options: { mode: string }) => BaseUIElement,
|
||||
content: (options: {
|
||||
mode: string
|
||||
resetScrollSignal: UIEventSource<void>
|
||||
}) => BaseUIElement,
|
||||
hashToShow: string,
|
||||
isShown: UIEventSource<boolean> = new UIEventSource<boolean>(false),
|
||||
options?: {
|
||||
setHash?: boolean
|
||||
}
|
||||
) {
|
||||
this.hashToShow = hashToShow
|
||||
this.isShown = isShown
|
||||
this._setHash = options?.setHash ?? true
|
||||
|
||||
if ((hashToShow === undefined || hashToShow === "") && this._setHash) {
|
||||
throw "HashToShow should be defined as it is vital for the 'back' key functionality"
|
||||
}
|
||||
|
||||
const mobileOptions = {
|
||||
mode: "mobile",
|
||||
resetScrollSignal: this._resetScrollSignal,
|
||||
}
|
||||
|
||||
this._fullscreencomponent = this.BuildComponent(
|
||||
title(mobileOptions),
|
||||
content(mobileOptions).SetClass("pb-20")
|
||||
)
|
||||
|
||||
const self = this
|
||||
if (this._setHash) {
|
||||
Hash.hash.addCallback((h) => {
|
||||
if (h === undefined) {
|
||||
isShown.setData(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
isShown.addCallbackD((isShown) => {
|
||||
if (isShown) {
|
||||
// We first must set the hash, then activate the panel
|
||||
// If the order is wrong, this will cause the panel to disactivate again
|
||||
ScrollableFullScreen._currentlyOpen = self
|
||||
self.Activate()
|
||||
} else {
|
||||
if (self.hashToShow !== undefined) {
|
||||
Hash.hash.setData(undefined)
|
||||
}
|
||||
// Some cleanup...
|
||||
ScrollableFullScreen.collapse()
|
||||
}
|
||||
})
|
||||
if (isShown.data) {
|
||||
ScrollableFullScreen._currentlyOpen = self
|
||||
this.Activate()
|
||||
}
|
||||
}
|
||||
|
||||
private static initEmpty(): FixedUiElement {
|
||||
Hotkeys.RegisterHotkey(
|
||||
{ nomod: "Escape", onUp: true },
|
||||
Translations.t.hotkeyDocumentation.closeSidebar,
|
||||
ScrollableFullScreen.collapse
|
||||
)
|
||||
|
||||
return new FixedUiElement("")
|
||||
}
|
||||
public static collapse() {
|
||||
const fs = document.getElementById("fullscreen")
|
||||
if (fs !== null) {
|
||||
ScrollableFullScreen.empty.AttachTo("fullscreen")
|
||||
fs.classList.add("hidden")
|
||||
}
|
||||
|
||||
const opened = ScrollableFullScreen._currentlyOpen
|
||||
if (opened !== undefined) {
|
||||
opened?.isShown?.setData(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Actually show this in the 'fullscreen'-div
|
||||
* @constructor
|
||||
*/
|
||||
public Activate(): void {
|
||||
if (this.hashToShow && this.hashToShow !== "" && this._setHash) {
|
||||
Hash.hash.setData(this.hashToShow)
|
||||
}
|
||||
this.isShown.setData(true)
|
||||
this._fullscreencomponent.AttachTo("fullscreen")
|
||||
const fs = document.getElementById("fullscreen")
|
||||
ScrollableFullScreen._currentlyOpen = this
|
||||
fs?.classList?.remove("hidden")
|
||||
}
|
||||
|
||||
private BuildComponent(title: BaseUIElement, content: BaseUIElement): BaseUIElement {
|
||||
const returnToTheMap = new Combine([
|
||||
Svg.back_svg().SetClass("block md:hidden w-12 h-12 p-2 svg-foreground"),
|
||||
Svg.close_svg().SetClass("hidden md:block w-12 h-12 p-3 svg-foreground"),
|
||||
]).SetClass("rounded-full p-0 flex-shrink-0 self-center")
|
||||
|
||||
returnToTheMap.onClick(() => {
|
||||
this.isShown.setData(false)
|
||||
Hash.hash.setData(undefined)
|
||||
})
|
||||
|
||||
title = new Title(title, 2)
|
||||
title.SetClass(
|
||||
"text-l sm:text-xl md:text-2xl w-full p-0 max-h-20vh overflow-y-auto self-center"
|
||||
)
|
||||
|
||||
const contentWrapper = new Combine([content]).SetClass(
|
||||
"block p-2 md:pt-4 w-full h-full overflow-y-auto"
|
||||
)
|
||||
|
||||
this._resetScrollSignal.addCallback((_) => {
|
||||
contentWrapper.ScrollToTop()
|
||||
})
|
||||
|
||||
return new Combine([
|
||||
new Combine([
|
||||
new Combine([returnToTheMap, title]).SetClass(
|
||||
"border-b-1 border-black shadow bg-white flex flex-shrink-0 pt-1 pb-1 md:pt-0 md:pb-0"
|
||||
),
|
||||
contentWrapper,
|
||||
// We add an ornament which takes around 5em. This is in order to make sure the Web UI doesn't hide
|
||||
]).SetClass("flex flex-col h-full relative bg-white"),
|
||||
]).SetClass(
|
||||
"fixed top-0 left-0 right-0 h-screen w-screen md:w-auto md:relative z-above-controls md:rounded-xl overflow-hidden"
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
import Translations from "../i18n/Translations"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "./Combine"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { VariableUiElement } from "./VariableUIElement"
|
||||
|
||||
export class TabbedComponent extends Combine {
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
constructor(
|
||||
elements: { header: BaseUIElement | string; content: BaseUIElement | string }[],
|
||||
openedTab: UIEventSource<number> | number = 0,
|
||||
options?: {
|
||||
leftOfHeader?: BaseUIElement
|
||||
styleHeader?: (header: BaseUIElement) => void
|
||||
}
|
||||
) {
|
||||
const openedTabSrc =
|
||||
typeof openedTab === "number"
|
||||
? new UIEventSource(openedTab)
|
||||
: openedTab ?? new UIEventSource<number>(0)
|
||||
|
||||
const tabs: BaseUIElement[] = [options?.leftOfHeader]
|
||||
const contentElements: BaseUIElement[] = []
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
let element = elements[i]
|
||||
const header = Translations.W(element.header).onClick(() => openedTabSrc.setData(i))
|
||||
openedTabSrc.addCallbackAndRun((selected) => {
|
||||
if (selected >= elements.length) {
|
||||
selected = 0
|
||||
}
|
||||
if (selected === i) {
|
||||
header.SetClass("tab-active")
|
||||
header.RemoveClass("tab-non-active")
|
||||
} else {
|
||||
header.SetClass("tab-non-active")
|
||||
header.RemoveClass("tab-active")
|
||||
}
|
||||
})
|
||||
const content = Translations.W(element.content)
|
||||
content.SetClass("relative w-full inline-block")
|
||||
contentElements.push(content)
|
||||
const tab = header.SetClass("block tab-single-header")
|
||||
tabs.push(tab)
|
||||
}
|
||||
|
||||
const header = new Combine(tabs).SetClass("tabs-header-bar")
|
||||
if (options?.styleHeader) {
|
||||
options.styleHeader(header)
|
||||
}
|
||||
const actualContent = new VariableUiElement(
|
||||
openedTabSrc.map((i) => contentElements[i])
|
||||
).SetStyle("max-height: inherit; height: inherit")
|
||||
super([header, actualContent])
|
||||
}
|
||||
}
|
|
@ -66,3 +66,15 @@
|
|||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
<style>
|
||||
.tab-selected {
|
||||
background-color: rgb(59 130 246);
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
.tab-unselected {
|
||||
background-color: rgb(255 255 255);
|
||||
color: rgb(0 0 0);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,62 +1,33 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import Translations from "../i18n/Translations"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { SubtleButton } from "../Base/SubtleButton"
|
||||
import Svg from "../../Svg"
|
||||
import ExportPDF from "../ExportPDF"
|
||||
import FilteredLayer from "../../Models/FilteredLayer"
|
||||
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import BaseLayer from "../../Models/BaseLayer"
|
||||
import Loc from "../../Models/Loc"
|
||||
|
||||
interface DownloadState {
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
featurePipeline: FeaturePipeline
|
||||
layoutToUse: LayoutConfig
|
||||
currentBounds: UIEventSource<BBox>
|
||||
backgroundLayer: UIEventSource<BaseLayer>
|
||||
locationControl: UIEventSource<Loc>
|
||||
featureSwitchExportAsPdf: UIEventSource<boolean>
|
||||
featureSwitchEnableExport: UIEventSource<boolean>
|
||||
}
|
||||
|
||||
export default class AllDownloads extends ScrollableFullScreen {
|
||||
export default class AllDownloads extends SubtleButton {
|
||||
constructor(
|
||||
isShown: UIEventSource<boolean>,
|
||||
state: {
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
featurePipeline: FeaturePipeline
|
||||
layoutToUse: LayoutConfig
|
||||
currentBounds: UIEventSource<BBox>
|
||||
backgroundLayer: UIEventSource<BaseLayer>
|
||||
locationControl: UIEventSource<Loc>
|
||||
featureSwitchExportAsPdf: UIEventSource<boolean>
|
||||
featureSwitchEnableExport: UIEventSource<boolean>
|
||||
}
|
||||
) {
|
||||
super(AllDownloads.GenTitle, () => AllDownloads.GeneratePanel(state), "downloads", isShown)
|
||||
}
|
||||
|
||||
private static GenTitle(): BaseUIElement {
|
||||
return Translations.t.general.download.title
|
||||
.Clone()
|
||||
.SetClass("text-2xl break-words font-bold p-2")
|
||||
}
|
||||
|
||||
private static GeneratePanel(state: DownloadState): BaseUIElement {
|
||||
const isExporting = new UIEventSource(false, "Pdf-is-exporting")
|
||||
const generatePdf = () => {
|
||||
isExporting.setData(true)
|
||||
new ExportPDF({
|
||||
freeDivId: "belowmap",
|
||||
background: state.backgroundLayer,
|
||||
location: state.locationControl,
|
||||
features: state.featurePipeline,
|
||||
layout: state.layoutToUse,
|
||||
}).isRunning.addCallbackAndRun((isRunning) => isExporting.setData(isRunning))
|
||||
}
|
||||
|
@ -78,6 +49,6 @@ export default class AllDownloads extends ScrollableFullScreen {
|
|||
isExporting
|
||||
)
|
||||
|
||||
return new SubtleButton(icon, text)
|
||||
super(icon, text)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,6 @@ import AllDownloads from "./AllDownloads"
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Lazy from "../Base/Lazy"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import FeatureInfoBox from "../Popup/FeatureInfoBox"
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
|
||||
export default class LeftControls extends Combine {
|
||||
|
@ -57,8 +55,6 @@ export default class LeftControls extends Combine {
|
|||
|
||||
new AllDownloads(guiState.downloadControlIsOpened, state)
|
||||
|
||||
|
||||
|
||||
super([currentViewAction])
|
||||
|
||||
this.SetClass("flex flex-col")
|
||||
|
|
|
@ -7,7 +7,6 @@ import WikidataPreviewBox from "../Wikipedia/WikidataPreviewBox"
|
|||
import { Button } from "../Base/Button"
|
||||
import Combine from "../Base/Combine"
|
||||
import Title from "../Base/Title"
|
||||
import WikipediaBox from "../Wikipedia/WikipediaBox"
|
||||
import Translations from "../i18n/Translations"
|
||||
import List from "../Base/List"
|
||||
import Svg from "../../Svg"
|
||||
|
@ -97,7 +96,7 @@ export default class PlantNetSpeciesSearch extends VariableUiElement {
|
|||
if (wikidataSpecies === undefined) {
|
||||
return plantOverview
|
||||
}
|
||||
const buttons = new Combine([
|
||||
return new Combine([
|
||||
new Button(
|
||||
new Combine([
|
||||
Svg.back_svg().SetClass(
|
||||
|
@ -120,15 +119,6 @@ export default class PlantNetSpeciesSearch extends VariableUiElement {
|
|||
}
|
||||
).SetClass("btn"),
|
||||
]).SetClass("flex justify-between")
|
||||
|
||||
return new Combine([
|
||||
new WikipediaBox([wikidataSpecies], {
|
||||
firstParagraphOnly: false,
|
||||
noImages: false,
|
||||
addHeader: false,
|
||||
}).SetClass("h-96"),
|
||||
buttons,
|
||||
]).SetClass("flex flex-col self-end")
|
||||
})
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
import { Utils } from "../Utils"
|
||||
import Toggle from "./Input/Toggle"
|
||||
import LeftControls from "./BigComponents/LeftControls"
|
||||
import RightControls from "./BigComponents/RightControls"
|
||||
import CenterMessageBox from "./CenterMessageBox"
|
||||
import ScrollableFullScreen from "./Base/ScrollableFullScreen"
|
||||
import Translations from "./i18n/Translations"
|
||||
import { DefaultGuiState } from "./DefaultGuiState"
|
||||
import Combine from "./Base/Combine"
|
||||
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
|
||||
import GeoLocationHandler from "../Logic/Actors/GeoLocationHandler"
|
||||
import CopyrightPanel from "./BigComponents/CopyrightPanel"
|
||||
|
||||
/**
|
||||
* The default MapComplete GUI initializer
|
||||
|
@ -25,17 +21,6 @@ export default class DefaultGUI {
|
|||
}
|
||||
|
||||
public setup() {
|
||||
this.SetupUIElements()
|
||||
|
||||
if (
|
||||
this.state.layoutToUse.customCss !== undefined &&
|
||||
window.location.pathname.indexOf("index") >= 0
|
||||
) {
|
||||
Utils.LoadCustomCss(this.state.layoutToUse.customCss)
|
||||
}
|
||||
}
|
||||
|
||||
private SetupUIElements() {
|
||||
const extraLink = Toggle.If(
|
||||
state.featureSwitchExtraLinkEnabled,
|
||||
() => new ExtraLinkButton(state, state.layoutToUse.extraLink)
|
||||
|
|
|
@ -14,8 +14,6 @@ import { VariableUiElement } from "../Base/VariableUIElement"
|
|||
import Loading from "../Base/Loading"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { DefaultGuiState } from "../DefaultGuiState"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
|
|
|
@ -11,33 +11,15 @@ import Loc from "../../Models/Loc"
|
|||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { FlowStep } from "./FlowStep"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import Title from "../Base/Title"
|
||||
import CheckBoxes from "../Input/Checkboxes"
|
||||
import AllTagsPanel from "../Popup/AllTagsPanel.svelte"
|
||||
import { Feature, Point } from "geojson"
|
||||
import DivContainer from "../Base/DivContainer"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import { AvailableRasterLayers, RasterLayerPolygon } from "../../Models/RasterLayers"
|
||||
import { MapLibreAdaptor } from "../Map/MapLibreAdaptor"
|
||||
import ShowDataLayer from "../Map/ShowDataLayer"
|
||||
|
||||
class PreviewPanel extends ScrollableFullScreen {
|
||||
constructor(tags: UIEventSource<any>) {
|
||||
super(
|
||||
(_) => new FixedUiElement("Element to import"),
|
||||
(_) =>
|
||||
new Combine([
|
||||
"The tags are:",
|
||||
new SvelteUIElement(AllTagsPanel, { tags }),
|
||||
]).SetClass("flex flex-col"),
|
||||
"element"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the data to import on a map, asks for the correct layer to be selected
|
||||
*/
|
||||
|
@ -111,7 +93,6 @@ export class MapPreview
|
|||
const currentBounds = new UIEventSource<BBox>(undefined)
|
||||
const { ui, mapproperties, map } = MapLibreAdaptor.construct()
|
||||
|
||||
|
||||
ui.SetClass("w-full").SetStyle("height: 500px")
|
||||
|
||||
layerPicker.GetValue().addCallbackAndRunD((layerToShow) => {
|
||||
|
@ -119,7 +100,6 @@ export class MapPreview
|
|||
layer: layerToShow,
|
||||
zoomToFeatures: true,
|
||||
features: new StaticFeatureSource(matching),
|
||||
buildPopup: (tag) => new PreviewPanel(tag),
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -34,62 +34,4 @@ export default class WikidataValidator extends Validator {
|
|||
}
|
||||
return out
|
||||
}
|
||||
|
||||
public inputHelper(currentValue, inputHelperOptions) {
|
||||
const args = inputHelperOptions.args ?? []
|
||||
const searchKey = 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: currentValue,
|
||||
searchText: searchForValue,
|
||||
instanceOf,
|
||||
notInstanceOf,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import ScrollableFullScreen from "../Base/ScrollableFullScreen"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
|
||||
import Svg from "../../Svg"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
export default class FeatureInfoBox extends ScrollableFullScreen {
|
||||
public constructor(
|
||||
tags: UIEventSource<any>,
|
||||
layerConfig: LayerConfig,
|
||||
state: FeaturePipelineState,
|
||||
options?: {
|
||||
hashToShow?: string
|
||||
isShown?: UIEventSource<boolean>
|
||||
setHash?: true | boolean
|
||||
}
|
||||
) {
|
||||
const showAllQuestions = state.featureSwitchShowAllQuestions.map(
|
||||
(fsShow) => fsShow || state.showAllQuestionsAtOnce.data,
|
||||
[state.showAllQuestionsAtOnce]
|
||||
)
|
||||
super(
|
||||
() => undefined,
|
||||
() => FeatureInfoBox.GenerateContent(tags, layerConfig),
|
||||
options?.hashToShow ?? tags.data.id ?? "item",
|
||||
options?.isShown,
|
||||
options
|
||||
)
|
||||
|
||||
if (layerConfig === undefined) {
|
||||
throw "Undefined layerconfig"
|
||||
}
|
||||
}
|
||||
|
||||
public static GenerateContent(tags: UIEventSource<any>): BaseUIElement {
|
||||
return new Toggle(
|
||||
new Combine([
|
||||
Svg.delete_icon_svg().SetClass("w-8 h-8"),
|
||||
Translations.t.delete.isDeleted,
|
||||
]).SetClass("flex justify-center font-bold items-center"),
|
||||
new Combine([]).SetClass("block"),
|
||||
tags.map((t) => t["_deleted"] == "yes")
|
||||
)
|
||||
}
|
||||
}
|
|
@ -19,14 +19,13 @@ import { ConflateButton, ImportPointButton, ImportWayButton } from "./Popup/Impo
|
|||
import TagApplyButton from "./Popup/TagApplyButton"
|
||||
import { CloseNoteButton } from "./Popup/CloseNoteButton"
|
||||
import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis"
|
||||
import { Stores, UIEventSource } from "../Logic/UIEventSource"
|
||||
import { Store, Stores, UIEventSource } from "../Logic/UIEventSource"
|
||||
import AllTagsPanel from "./Popup/AllTagsPanel.svelte"
|
||||
import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"
|
||||
import { ImageCarousel } from "./Image/ImageCarousel"
|
||||
import { ImageUploadFlow } from "./Image/ImageUploadFlow"
|
||||
import { VariableUiElement } from "./Base/VariableUIElement"
|
||||
import { Utils } from "../Utils"
|
||||
import WikipediaBox from "./Wikipedia/WikipediaBox"
|
||||
import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata"
|
||||
import { Translation } from "./i18n/Translation"
|
||||
import Translations from "./i18n/Translations"
|
||||
|
@ -80,6 +79,7 @@ import { OsmId, OsmTags, WayId } from "../Models/OsmFeature"
|
|||
import MoveWizard from "./Popup/MoveWizard"
|
||||
import SplitRoadWizard from "./Popup/SplitRoadWizard"
|
||||
import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz"
|
||||
import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte"
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
@ -628,7 +628,7 @@ export default class SpecialVisualizations {
|
|||
|
||||
{
|
||||
funcName: "wikipedia",
|
||||
docs: "A box showing the corresponding wikipedia article - based on the wikidata tag",
|
||||
docs: "A box showing the corresponding wikipedia article(s) - based on the **wikidata** tag.",
|
||||
args: [
|
||||
{
|
||||
name: "keyToShowWikipediaFor",
|
||||
|
@ -638,23 +638,15 @@ export default class SpecialVisualizations {
|
|||
],
|
||||
example:
|
||||
"`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height",
|
||||
constr: (_, tagsSource, args) => {
|
||||
constr: (_, tagsSource, args, feature, layer) => {
|
||||
const keys = args[0].split(";").map((k) => k.trim())
|
||||
return new VariableUiElement(
|
||||
tagsSource
|
||||
.map((tags) => {
|
||||
const key = keys.find(
|
||||
(k) => tags[k] !== undefined && tags[k] !== ""
|
||||
)
|
||||
return tags[key]
|
||||
})
|
||||
.map((wikidata) => {
|
||||
const wikidatas: string[] = Utils.NoEmpty(
|
||||
wikidata?.split(";")?.map((wd) => wd.trim()) ?? []
|
||||
)
|
||||
return new WikipediaBox(wikidatas)
|
||||
})
|
||||
)
|
||||
const wikiIds: Store<string[]> = tagsSource.map((tags) => {
|
||||
const key = keys.find((k) => tags[k] !== undefined && tags[k] !== "")
|
||||
return tags[key]?.split(";")?.map((id) => id.trim())
|
||||
})
|
||||
return new SvelteUIElement(WikipediaPanel, {
|
||||
wikiIds,
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -276,17 +276,3 @@
|
|||
</FloatOver>
|
||||
</If>
|
||||
|
||||
|
||||
<style>
|
||||
/* WARNING: This is just for demonstration.
|
||||
Using :global() in this way can be risky. */
|
||||
:global(.tab-selected) {
|
||||
background-color: rgb(59 130 246);
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
:global(.tab-unselected) {
|
||||
background-color: rgb(255 255 255);
|
||||
color: rgb(0 0 0);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -8,20 +8,12 @@ import Locale from "../i18n/Locale"
|
|||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import WikidataPreviewBox from "./WikidataPreviewBox"
|
||||
import Title from "../Base/Title"
|
||||
import WikipediaBox from "./WikipediaBox"
|
||||
import Svg from "../../Svg"
|
||||
import Loading from "../Base/Loading"
|
||||
import Table from "../Base/Table"
|
||||
|
||||
export default class WikidataSearchBox extends InputElement<string> {
|
||||
private static readonly _searchCache = new Map<string, Promise<WikidataResponse[]>>()
|
||||
private readonly wikidataId: UIEventSource<string>
|
||||
private readonly searchText: UIEventSource<string>
|
||||
private readonly instanceOf?: number[]
|
||||
private readonly notInstanceOf?: number[]
|
||||
|
||||
public static docs = new Combine([
|
||||
,
|
||||
new Title("Helper arguments"),
|
||||
new Table(
|
||||
["name", "doc"],
|
||||
|
@ -100,6 +92,11 @@ Another example is to search for species and trees:
|
|||
\`\`\`
|
||||
`,
|
||||
])
|
||||
private static readonly _searchCache = new Map<string, Promise<WikidataResponse[]>>()
|
||||
private readonly wikidataId: UIEventSource<string>
|
||||
private readonly searchText: UIEventSource<string>
|
||||
private readonly instanceOf?: number[]
|
||||
private readonly notInstanceOf?: number[]
|
||||
|
||||
constructor(options?: {
|
||||
searchText?: UIEventSource<string>
|
||||
|
@ -207,25 +204,15 @@ Another example is to search for species and trees:
|
|||
)
|
||||
)
|
||||
|
||||
const full = new Combine([
|
||||
return 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.split(";"))
|
||||
})
|
||||
).SetStyle("max-height:12.5rem"),
|
||||
full,
|
||||
]).ConstructElement()
|
||||
])
|
||||
.SetClass("flex flex-col border-2 border-black rounded-xl m-2 p-2")
|
||||
.ConstructElement()
|
||||
}
|
||||
}
|
||||
|
|
46
UI/Wikipedia/WikipediaArticle.svelte
Normal file
46
UI/Wikipedia/WikipediaArticle.svelte
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import type { FullWikipediaDetails } from "../../Logic/Web/Wikipedia";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import FromHtml from "../Base/FromHtml.svelte";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
import { Disclosure, DisclosureButton, DisclosurePanel } from "@rgossiaux/svelte-headlessui";
|
||||
import { ChevronRightIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import WikidataPreviewBox from "./WikidataPreviewBox";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
|
||||
/**
|
||||
* Small helper
|
||||
*/
|
||||
export let wikipediaDetails: Store<FullWikipediaDetails>;
|
||||
</script>
|
||||
|
||||
<a href={$wikipediaDetails.articleUrl} target="_blank" rel="noreferrer" class="flex">
|
||||
<img src="./assets/svg/wikipedia.svg" class="w-6 h-6"/>
|
||||
<Tr t={Translations.t.general.wikipedia.fromWikipedia}/>
|
||||
</a>
|
||||
|
||||
{#if $wikipediaDetails.wikidata}
|
||||
<ToSvelte construct={WikidataPreviewBox.WikidataResponsePreview($wikipediaDetails.wikidata)} />
|
||||
{/if}
|
||||
|
||||
{#if $wikipediaDetails.firstParagraph === "" || $wikipediaDetails.firstParagraph === undefined}
|
||||
<Loading >
|
||||
<Tr t={Translations.t.general.wikipedia.loading}/>
|
||||
</Loading>
|
||||
{:else}
|
||||
<FromHtml src={$wikipediaDetails.firstParagraph} />
|
||||
<Disclosure let:open>
|
||||
<DisclosureButton>
|
||||
<span class="flex">
|
||||
<ChevronRightIcon class="w-6 h-6" style={open ? "transform: rotate(90deg);" : ""} />
|
||||
Read the rest of the article
|
||||
|
||||
</span>
|
||||
</DisclosureButton>
|
||||
<DisclosurePanel>
|
||||
<FromHtml src={$wikipediaDetails.restOfArticle} />
|
||||
</DisclosurePanel>
|
||||
</Disclosure>
|
||||
{/if}
|
|
@ -1,321 +0,0 @@
|
|||
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 { Store, 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"
|
||||
import { Paragraph } from "../Base/Paragraph"
|
||||
|
||||
export interface WikipediaBoxOptions {
|
||||
addHeader: boolean
|
||||
firstParagraphOnly: boolean
|
||||
noImages: boolean
|
||||
currentState?: UIEventSource<"loading" | "loaded" | "error">
|
||||
}
|
||||
|
||||
export default class WikipediaBox extends Combine {
|
||||
constructor(wikidataIds: string[], options?: WikipediaBoxOptions) {
|
||||
const mainContents = []
|
||||
options = options ?? { addHeader: false, firstParagraphOnly: true, noImages: false }
|
||||
const pages = wikidataIds.map((entry) =>
|
||||
WikipediaBox.createLinkedContent(entry.trim(), options)
|
||||
)
|
||||
if (wikidataIds.length == 1) {
|
||||
const page = pages[0]
|
||||
mainContents.push(
|
||||
new Combine([
|
||||
new Combine([
|
||||
options.noImages
|
||||
? undefined
|
||||
: Svg.wikipedia_ui()
|
||||
.SetStyle("width: 1.5rem")
|
||||
.SetClass("inline-block mr-3"),
|
||||
page.titleElement,
|
||||
]).SetClass("flex"),
|
||||
page.linkElement,
|
||||
]).SetClass("flex justify-between align-middle")
|
||||
)
|
||||
mainContents.push(page.contents.SetClass("overflow-auto normal-background rounded-lg"))
|
||||
} else if (wikidataIds.length > 1) {
|
||||
const tabbed = new TabbedComponent(
|
||||
pages.map((page) => {
|
||||
const contents = page.contents
|
||||
.SetClass("overflow-auto normal-background rounded-lg block")
|
||||
.SetStyle("max-height: inherit; height: inherit; padding-bottom: 3.3rem")
|
||||
return {
|
||||
header: page.titleElement.SetClass("pl-2 pr-2"),
|
||||
content: new Combine([
|
||||
page.linkElement
|
||||
.SetStyle("top: 2rem; right: 2.5rem;")
|
||||
.SetClass(
|
||||
"absolute subtle-background rounded-full p-3 opacity-50 hover:opacity-100 transition-opacity"
|
||||
),
|
||||
contents,
|
||||
])
|
||||
.SetStyle("max-height: inherit; height: inherit")
|
||||
.SetClass("relative"),
|
||||
}
|
||||
}),
|
||||
0,
|
||||
{
|
||||
leftOfHeader: options.noImages
|
||||
? undefined
|
||||
: Svg.wikipedia_svg()
|
||||
.SetStyle("width: 1.5rem; align-self: center;")
|
||||
.SetClass("mr-4"),
|
||||
styleHeader: (header) =>
|
||||
header.SetClass("subtle-background").SetStyle("height: 3.3rem"),
|
||||
}
|
||||
)
|
||||
tabbed.SetStyle("height: inherit; max-height: inherit; overflow: hidden")
|
||||
mainContents.push(tabbed)
|
||||
}
|
||||
|
||||
super(mainContents)
|
||||
|
||||
this.SetClass("block rounded-xl subtle-background m-1 p-2 flex flex-col").SetStyle(
|
||||
"max-height: inherit"
|
||||
)
|
||||
}
|
||||
|
||||
private static createLinkedContent(
|
||||
entry: string,
|
||||
options: WikipediaBoxOptions
|
||||
): {
|
||||
titleElement: BaseUIElement
|
||||
contents: BaseUIElement
|
||||
linkElement: BaseUIElement
|
||||
} {
|
||||
if (entry.match("[qQ][0-9]+")) {
|
||||
return WikipediaBox.createWikidatabox(entry, options)
|
||||
} else {
|
||||
return WikipediaBox.createWikipediabox(entry, options)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a '<language>:<article-name>'-string, constructs the wikipedia article
|
||||
*/
|
||||
private static createWikipediabox(
|
||||
wikipediaArticle: string,
|
||||
options: WikipediaBoxOptions
|
||||
): {
|
||||
titleElement: BaseUIElement
|
||||
contents: BaseUIElement
|
||||
linkElement: BaseUIElement
|
||||
} {
|
||||
const wp = Translations.t.general.wikipedia
|
||||
|
||||
const article = Wikipedia.extractLanguageAndName(wikipediaArticle)
|
||||
if (article === undefined) {
|
||||
return {
|
||||
titleElement: undefined,
|
||||
contents: wp.noWikipediaPage,
|
||||
linkElement: undefined,
|
||||
}
|
||||
}
|
||||
const wikipedia = new Wikipedia({ language: article.language })
|
||||
const url = wikipedia.getPageUrl(article.pageName)
|
||||
const linkElement = new Link(
|
||||
Svg.pop_out_svg().SetStyle("width: 1.2rem").SetClass("block "),
|
||||
url,
|
||||
true
|
||||
).SetClass("flex items-center enable-links")
|
||||
|
||||
return {
|
||||
titleElement: new Title(article.pageName, 3),
|
||||
contents: WikipediaBox.createContents(article.pageName, wikipedia, options),
|
||||
linkElement,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a `Q1234`, constructs a wikipedia box (if a wikipedia page is available) or wikidata box as fallback.
|
||||
*
|
||||
*/
|
||||
private static createWikidatabox(
|
||||
wikidataId: string,
|
||||
options: WikipediaBoxOptions
|
||||
): {
|
||||
titleElement: BaseUIElement
|
||||
contents: BaseUIElement
|
||||
linkElement: BaseUIElement
|
||||
} {
|
||||
const wp = Translations.t.general.wikipedia
|
||||
|
||||
const wikiLink: Store<
|
||||
| [string, string, WikidataResponse]
|
||||
| "loading"
|
||||
| "failed"
|
||||
| ["no page", WikidataResponse]
|
||||
> = Wikidata.LoadWikidataEntry(wikidataId).map(
|
||||
(maybewikidata) => {
|
||||
if (maybewikidata === undefined) {
|
||||
return "loading"
|
||||
}
|
||||
if (maybewikidata["error"] !== undefined) {
|
||||
return "failed"
|
||||
}
|
||||
const wikidata = <WikidataResponse>maybewikidata["success"]
|
||||
if (wikidata === undefined) {
|
||||
return "failed"
|
||||
}
|
||||
if (wikidata.wikisites.size === 0) {
|
||||
return ["no page", wikidata]
|
||||
}
|
||||
|
||||
const preferredLanguage = [
|
||||
Locale.language.data,
|
||||
"en",
|
||||
Array.from(wikidata.wikisites.keys())[0],
|
||||
]
|
||||
let language
|
||||
let pagetitle
|
||||
let i = 0
|
||||
do {
|
||||
language = preferredLanguage[i]
|
||||
pagetitle = wikidata.wikisites.get(language)
|
||||
i++
|
||||
} while (pagetitle === undefined)
|
||||
return [pagetitle, language, wikidata]
|
||||
},
|
||||
[Locale.language]
|
||||
)
|
||||
|
||||
const contents = new VariableUiElement(
|
||||
wikiLink.map((status) => {
|
||||
if (status === "loading") {
|
||||
return new Loading(wp.loading.Clone()).SetClass("pl-6 pt-2")
|
||||
}
|
||||
|
||||
if (status === "failed") {
|
||||
return wp.failed.Clone().SetClass("alert p-4")
|
||||
}
|
||||
if (status[0] == "no page") {
|
||||
const [_, wd] = <[string, WikidataResponse]>status
|
||||
options.currentState?.setData("loaded")
|
||||
return new Combine([
|
||||
WikidataPreviewBox.WikidataResponsePreview(wd),
|
||||
wp.noWikipediaPage.Clone().SetClass("subtle"),
|
||||
]).SetClass("flex flex-col p-4")
|
||||
}
|
||||
|
||||
const [pagetitle, language, wd] = <[string, string, WikidataResponse]>status
|
||||
const wikipedia = new Wikipedia({ language })
|
||||
const quickFacts = WikidataPreviewBox.QuickFacts(wd)
|
||||
return WikipediaBox.createContents(pagetitle, wikipedia, {
|
||||
topBar: quickFacts,
|
||||
...options,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
const titleElement = new VariableUiElement(
|
||||
wikiLink.map((state) => {
|
||||
if (typeof state !== "string") {
|
||||
const [pagetitle, _] = state
|
||||
if (pagetitle === "no page") {
|
||||
const wd = <WikidataResponse>state[1]
|
||||
return new Title(Translation.fromMap(wd.labels), 3)
|
||||
}
|
||||
return new Title(pagetitle, 3)
|
||||
}
|
||||
return new Link(
|
||||
new Title(wikidataId, 3),
|
||||
"https://www.wikidata.org/wiki/" + wikidataId,
|
||||
true
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
const linkElement = new VariableUiElement(
|
||||
wikiLink.map((state) => {
|
||||
if (typeof state !== "string") {
|
||||
const [pagetitle, language] = state
|
||||
const popout = options.noImages
|
||||
? "Source"
|
||||
: Svg.pop_out_svg().SetStyle("width: 1.2rem").SetClass("block")
|
||||
if (pagetitle === "no page") {
|
||||
const wd = <WikidataResponse>state[1]
|
||||
return new Link(popout, "https://www.wikidata.org/wiki/" + wd.id, true)
|
||||
}
|
||||
|
||||
const url = `https://${language}.wikipedia.org/wiki/${pagetitle}`
|
||||
return new Link(popout, url, true)
|
||||
}
|
||||
return undefined
|
||||
})
|
||||
).SetClass("flex items-center enable-links")
|
||||
|
||||
return {
|
||||
contents: contents,
|
||||
linkElement: linkElement,
|
||||
titleElement: titleElement,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual content in a scrollable way for the given wikipedia page
|
||||
*/
|
||||
private static createContents(
|
||||
pagename: string,
|
||||
wikipedia: Wikipedia,
|
||||
options: {
|
||||
topBar?: BaseUIElement
|
||||
} & WikipediaBoxOptions
|
||||
): BaseUIElement {
|
||||
const htmlContent = wikipedia.GetArticle(pagename, options)
|
||||
const wp = Translations.t.general.wikipedia
|
||||
const contents: VariableUiElement = new VariableUiElement(
|
||||
htmlContent.map((htmlContent) => {
|
||||
if (htmlContent === undefined) {
|
||||
// Still loading
|
||||
return new Loading(wp.loading.Clone())
|
||||
}
|
||||
if (htmlContent["success"] !== undefined) {
|
||||
let content: BaseUIElement = new FixedUiElement(htmlContent["success"])
|
||||
if (options?.addHeader) {
|
||||
content = new Combine([
|
||||
new Paragraph(
|
||||
new Link(wp.fromWikipedia, wikipedia.getPageUrl(pagename), true)
|
||||
),
|
||||
new Paragraph(content),
|
||||
])
|
||||
}
|
||||
return content.SetClass("wikipedia-article")
|
||||
}
|
||||
if (htmlContent["error"]) {
|
||||
console.warn("Loading wikipage failed due to", htmlContent["error"])
|
||||
return wp.failed.Clone().SetClass("alert p-4")
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
||||
)
|
||||
|
||||
htmlContent.addCallbackAndRunD((c) => {
|
||||
if (c["success"] !== undefined) {
|
||||
options.currentState?.setData("loaded")
|
||||
} else if (c["error"] !== undefined) {
|
||||
options.currentState?.setData("error")
|
||||
} else {
|
||||
options.currentState?.setData("loading")
|
||||
}
|
||||
})
|
||||
|
||||
return new Combine([
|
||||
options?.topBar?.SetClass("border-2 border-grey rounded-lg m-1 mb-0"),
|
||||
contents.SetClass("block pl-6 pt-2"),
|
||||
])
|
||||
}
|
||||
}
|
7
UI/Wikipedia/WikipediaBoxOptions.ts
Normal file
7
UI/Wikipedia/WikipediaBoxOptions.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
export interface WikipediaBoxOptions {
|
||||
addHeader?: boolean
|
||||
firstParagraphOnly?: true | boolean
|
||||
allowToAdd?: boolean
|
||||
}
|
56
UI/Wikipedia/WikipediaPanel.svelte
Normal file
56
UI/Wikipedia/WikipediaPanel.svelte
Normal file
|
@ -0,0 +1,56 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Shows one or more wikidata info boxes or wikipedia articles in a tabbed component.
|
||||
*/
|
||||
import type { FullWikipediaDetails } from "../../Logic/Web/Wikipedia";
|
||||
import Wikipedia from "../../Logic/Web/Wikipedia";
|
||||
import Locale from "../i18n/Locale";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@rgossiaux/svelte-headlessui";
|
||||
import WikipediaTitle from "./WikipediaTitle.svelte";
|
||||
import WikipediaArticle from "./WikipediaArticle.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
|
||||
/**
|
||||
* Either a wikidata item or a '<language>:<article>' link
|
||||
*/
|
||||
export let wikiIds: Store<string[]>;
|
||||
let wikipediaStores: Store<Store<FullWikipediaDetails>[]> = Locale.language.bind(language =>
|
||||
wikiIds.map(wikiIds => wikiIds.map(id => Wikipedia.fetchArticleAndWikidata(id, language))));
|
||||
let _wikipediaStores;
|
||||
onDestroy(wikipediaStores.addCallbackAndRunD(wikipediaStores => {
|
||||
_wikipediaStores = wikipediaStores;
|
||||
}));
|
||||
</script>
|
||||
{#if _wikipediaStores !== undefined}
|
||||
<TabGroup>
|
||||
<TabList>
|
||||
{#each _wikipediaStores as store (store.tag)}
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<WikipediaTitle wikipediaDetails={store} />
|
||||
</Tab>
|
||||
{/each}
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
|
||||
{#each _wikipediaStores as store (store.tag)}
|
||||
<TabPanel>
|
||||
<WikipediaArticle wikipediaDetails={store} />
|
||||
|
||||
</TabPanel>
|
||||
{/each}
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
{/if}
|
||||
<style>
|
||||
.tab-selected {
|
||||
background-color: rgb(59 130 246);
|
||||
color: rgb(255 255 255);
|
||||
}
|
||||
|
||||
.tab-unselected {
|
||||
background-color: rgb(255 255 255);
|
||||
color: rgb(0 0 0);
|
||||
}
|
||||
</style>
|
13
UI/Wikipedia/WikipediaTitle.svelte
Normal file
13
UI/Wikipedia/WikipediaTitle.svelte
Normal file
|
@ -0,0 +1,13 @@
|
|||
<script lang="ts">
|
||||
import type { FullWikipediaDetails } from "../../Logic/Web/Wikipedia";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
/**
|
||||
* Small helper
|
||||
*/
|
||||
export let wikipediaDetails: Store<FullWikipediaDetails>
|
||||
</script>
|
||||
|
||||
{$wikipediaDetails.title}
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue