Merge develop

This commit is contained in:
Pieter Vander Vennet 2024-05-02 23:56:31 +02:00
commit ef3e27ee8b
399 changed files with 38592 additions and 44846 deletions

View file

@ -0,0 +1,17 @@
<script lang="ts">
import { UIEventSource } from "../../Logic/UIEventSource"
import { marked } from "marked"
export let src: string
export let srcWritable: UIEventSource<string> = undefined
srcWritable?.addCallbackAndRunD(t => {
src = t
})
if(src !== undefined && typeof src !== "string") {
console.trace("Got a non-string object in Markdown", src)
throw "Markdown.svelte got a non-string object"
}
</script>
{#if src?.length > 0}
{@html marked.parse(src)}
{/if}

View file

@ -1,73 +1,21 @@
import Combine from "./Combine"
import BaseUIElement from "../BaseUIElement"
import { Translation } from "../i18n/Translation"
import { FixedUiElement } from "./FixedUiElement"
import Title from "./Title"
import List from "./List"
import Link from "./Link"
import { marked } from "marked"
import { parse as parse_html } from "node-html-parser"
import {default as turndown} from "turndown"
import { Utils } from "../../Utils"
export default class TableOfContents extends Combine {
private readonly titles: Title[]
export default class TableOfContents {
constructor(
elements: Combine | Title[],
options?: {
noTopLevel: false | boolean
maxDepth?: number
}
) {
let titles: Title[]
if (elements instanceof Combine) {
titles = TableOfContents.getTitles(elements.getElements()) ?? []
} else {
titles = elements ?? []
}
let els: { level: number; content: BaseUIElement }[] = []
for (const title of titles) {
let content: BaseUIElement
if (title.title instanceof Translation) {
content = title.title.Clone()
} else if (title.title instanceof FixedUiElement) {
content = new FixedUiElement(title.title.content)
} else if (Utils.runningFromConsole) {
content = new FixedUiElement(title.AsMarkdown())
} else if (title["title"] !== undefined) {
content = new FixedUiElement(title.title.ConstructElement().textContent)
} else {
console.log("Not generating a title for ", title)
continue
}
const vis = new Link(content, "#" + title.id)
els.push({ level: title.level, content: vis })
}
const minLevel = Math.min(...els.map((e) => e.level))
if (options?.noTopLevel) {
els = els.filter((e) => e.level !== minLevel)
}
if (options?.maxDepth) {
els = els.filter((e) => e.level <= options.maxDepth + minLevel)
}
super(TableOfContents.mergeLevel(els).map((el) => el.SetClass("mt-2")))
this.SetClass("flex flex-col")
this.titles = titles
}
private static getTitles(elements: BaseUIElement[]): Title[] {
const titles = []
for (const uiElement of elements) {
if (uiElement instanceof Combine) {
titles.push(...TableOfContents.getTitles(uiElement.getElements()))
} else if (uiElement instanceof Title) {
titles.push(uiElement)
}
}
return titles
private static asLinkableId(text: string): string {
return text
?.replace(/ /g, "-")
?.replace(/[?#.;:/]/, "")
?.toLowerCase() ?? ""
}
private static mergeLevel(
@ -88,7 +36,7 @@ export default class TableOfContents extends Combine {
if (running.length !== undefined) {
result.push({
content: new List(running),
level: maxLevel - 1,
level: maxLevel - 1
})
running = []
}
@ -97,24 +45,81 @@ export default class TableOfContents extends Combine {
if (running.length !== undefined) {
result.push({
content: new List(running),
level: maxLevel - 1,
level: maxLevel - 1
})
}
return TableOfContents.mergeLevel(result)
}
AsMarkdown(): string {
const depthIcons = ["1.", " -", " +", " *"]
const lines = ["## Table of contents\n"]
const minLevel = Math.min(...this.titles.map((t) => t.level))
for (const title of this.titles) {
const prefix = depthIcons[title.level - minLevel] ?? " ~"
const text = title.title.AsMarkdown().replace("\n", "")
const link = title.id
lines.push(prefix + " [" + text + "](#" + link + ")")
public static insertTocIntoMd(md: string): string {
const htmlSource = <string>marked.parse(md)
const el = parse_html(htmlSource)
const structure = TableOfContents.generateStructure(<any>el)
let firstTitle = structure[1]
let minDepth = undefined
do {
minDepth = Math.min(...structure.map(s => s.depth))
const minDepthCount = structure.filter(s => s.depth === minDepth)
if (minDepthCount.length > 1) {
break
}
// Erase a single top level heading
structure.splice(structure.findIndex(s => s.depth === minDepth), 1)
} while (structure.length > 0)
if (structure.length <= 1) {
return md
}
const separators = {
1: " -",
2: " +",
3: " *"
}
return lines.join("\n") + "\n\n"
let toc = ""
let topLevelCount = 0
for (const el of structure) {
const depthDiff = el.depth - minDepth
let link = `[${el.title}](#${TableOfContents.asLinkableId(el.title)})`
if (depthDiff === 0) {
topLevelCount++
toc += `${topLevelCount}. ${link}\n`
} else if (depthDiff <= 3) {
toc += `${separators[depthDiff]} ${link}\n`
}
}
const heading = Utils.Times(() => "#", firstTitle.depth)
toc = heading +" Table of contents\n\n"+toc
const original = el.outerHTML
const firstTitleIndex = original.indexOf(firstTitle.el.outerHTML)
const tocHtml = (<string>marked.parse(toc))
const withToc = original.substring(0, firstTitleIndex) + tocHtml + original.substring(firstTitleIndex)
const htmlToMd = new turndown()
return htmlToMd.turndown(withToc)
}
public static generateStructure(html: Element): { depth: number, title: string, el: Element }[] {
if (html === undefined) {
return []
}
return [].concat(...Array.from(html.childNodes ?? []).map(
child => {
const tag: string = child["tagName"]?.toLowerCase()
if (!tag) {
return []
}
if (tag.match(/h[0-9]/)) {
const depth = Number(tag.substring(1))
return [{ depth, title: child.textContent, el: child }]
}
return TableOfContents.generateStructure(<Element>child)
}
))
}
}

View file

@ -0,0 +1,56 @@
<script lang="ts">
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"
import Locale from "../i18n/Locale"
import { Utils } from "../../Utils"
import Translations from "../i18n/Translations"
import type { SpecialVisualizationState } from "../SpecialVisualization"
import Pop_out from "../../assets/svg/Pop_out.svelte"
import Tr from "../Base/Tr.svelte"
import Icon from "../Map/Icon.svelte"
export let state: SpecialVisualizationState
let theme = state.layout?.id ?? ""
let config: ExtraLinkConfig = state.layout.extraLink
console.log(">>>",config)
const isIframe = window !== window.top
let basepath = window.location.host
let showWelcomeMessageSwitch = state.featureSwitches.featureSwitchWelcomeMessage
const t = Translations.t.general
const href = state.mapProperties.location.map(
(loc) => {
const subs = {
...loc,
theme: theme,
basepath,
language: Locale.language.data
}
return Utils.SubstituteKeys(config.href, subs)
},
[state.mapProperties.zoom]
)
</script>
{#if config !== undefined &&
!(config.requirements.has("iframe") && !isIframe) &&
!(config.requirements.has("no-iframe") && isIframe) &&
!(config.requirements.has("welcome-message") && !$showWelcomeMessageSwitch) &&
!(config.requirements.has("no-welcome-message") && $showWelcomeMessageSwitch)}
<div class="links-as-button">
<a href={$href} target={config.newTab ? "_blank" : ""} rel="noopener"
class="flex pointer-events-auto rounded-full border-black">
<Icon icon={config.icon} clss="w-6 h-6 m-2"/>
{#if config.text}
<Tr t={config.text} />
{:else}
<Tr t={t.screenToSmall.Subs({theme: state.layout.title})} />
{/if}
</a>
</div>
{/if}

View file

@ -1,101 +0,0 @@
import { UIElement } from "../UIElement"
import BaseUIElement from "../BaseUIElement"
import { Store } from "../../Logic/UIEventSource"
import ExtraLinkConfig from "../../Models/ThemeConfig/ExtraLinkConfig"
import Img from "../Base/Img"
import { SubtleButton } from "../Base/SubtleButton"
import Toggle from "../Input/Toggle"
import Locale from "../i18n/Locale"
import { Utils } from "../../Utils"
import Svg from "../../Svg"
import Translations from "../i18n/Translations"
import { Translation } from "../i18n/Translation"
interface ExtraLinkButtonState {
layout: { id: string; title: Translation }
featureSwitches: { featureSwitchWelcomeMessage: Store<boolean> }
mapProperties: {
location: Store<{ lon: number; lat: number }>
zoom: Store<number>
}
}
export default class ExtraLinkButton extends UIElement {
private readonly _config: ExtraLinkConfig
private readonly state: ExtraLinkButtonState
constructor(state: ExtraLinkButtonState, config: ExtraLinkConfig) {
super()
this.state = state
this._config = config
}
protected InnerRender(): BaseUIElement {
if (this._config === undefined) {
return undefined
}
const c = this._config
const isIframe = window !== window.top
if (c.requirements?.has("iframe") && !isIframe) {
return undefined
}
if (c.requirements?.has("no-iframe") && isIframe) {
return undefined
}
let link: BaseUIElement
const theme = this.state.layout?.id ?? ""
const basepath = window.location.host
const href = this.state.mapProperties.location.map(
(loc) => {
const subs = {
...loc,
theme: theme,
basepath,
language: Locale.language.data,
}
return Utils.SubstituteKeys(c.href, subs)
},
[this.state.mapProperties.zoom]
)
let img: BaseUIElement = Svg.pop_out_svg()
if (c.icon !== undefined) {
img = new Img(c.icon).SetClass("h-6")
}
let text: Translation
if (c.text === undefined) {
text = Translations.t.general.screenToSmall.Subs({
theme: this.state.layout.title,
})
} else {
text = c.text.Clone()
}
link = new SubtleButton(img, text, {
url: href,
newTab: c.newTab,
})
if (c.requirements?.has("no-welcome-message")) {
link = new Toggle(
undefined,
link,
this.state.featureSwitches.featureSwitchWelcomeMessage
)
}
if (c.requirements?.has("welcome-message")) {
link = new Toggle(
link,
undefined,
this.state.featureSwitches.featureSwitchWelcomeMessage
)
}
return link
}
}

View file

@ -119,7 +119,7 @@
<Tr t={t.conflicting.intro} />
{/if}
{#if $different.length > 0}
{#each $different as key}
{#each $different as key (key)}
<div class="mx-2 rounded-2xl">
<ComparisonAction
{key}
@ -136,7 +136,7 @@
{#if $missing.length > 0}
{#if currentStep === "init"}
{#each $missing as key}
{#each $missing as key (key)}
<div class:glowing-shadow={applyAllHovered} class="mx-2 rounded-2xl">
<ComparisonAction
{key}
@ -174,7 +174,7 @@
{#if readonly}
<div class="w-full overflow-x-auto">
<div class="flex h-32 w-max gap-x-2">
{#each $unknownImages as image}
{#each $unknownImages as image (image)}
<AttributedImage
imgClass="h-32 w-max shrink-0"
image={{ url: image }}
@ -184,7 +184,7 @@
</div>
</div>
{:else}
{#each $unknownImages as image}
{#each $unknownImages as image (image)}
<LinkableImage
{tags}
{state}

View file

@ -1,46 +0,0 @@
import { InputElement } from "./InputElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import Translations from "../i18n/Translations"
import BaseUIElement from "../BaseUIElement"
/**
* @deprecated
*/
export class FixedInputElement<T> extends InputElement<T> {
private readonly value: UIEventSource<T>
private readonly _comparator: (t0: T, t1: T) => boolean
private readonly _el: HTMLElement
constructor(
rendering: BaseUIElement | string,
value: T | UIEventSource<T>,
comparator: (t0: T, t1: T) => boolean = undefined
) {
super()
this._comparator = comparator ?? ((t0, t1) => t0 == t1)
if (value instanceof UIEventSource) {
this.value = value
} else {
this.value = new UIEventSource<T>(value)
}
this._el = document.createElement("span")
const e = Translations.W(rendering)?.ConstructElement()
if (e) {
this._el.appendChild(e)
}
}
GetValue(): UIEventSource<T> {
return this.value
}
IsValid(t: T): boolean {
return this._comparator(t, this.value.data)
}
protected InnerConstructElement(): HTMLElement {
return this._el
}
}

View file

@ -1,161 +0,0 @@
import { InputElement } from "./InputElement"
import { UIEventSource } from "../../Logic/UIEventSource"
import { Utils } from "../../Utils"
/**
* @deprecated
*/
export class RadioButton<T> extends InputElement<T> {
private static _nextId = 0
private readonly value: UIEventSource<T>
private _elements: InputElement<T>[]
private _selectFirstAsDefault: boolean
private _dontStyle: boolean
constructor(
elements: InputElement<T>[],
options?: {
selectFirstAsDefault?: true | boolean
dontStyle?: boolean
value?: UIEventSource<T>
}
) {
super()
options = options ?? {}
this._selectFirstAsDefault = options.selectFirstAsDefault ?? true
this._elements = Utils.NoNull(elements)
this.value = options?.value ?? new UIEventSource<T>(undefined)
this._dontStyle = options.dontStyle ?? false
}
IsValid(t: T): boolean {
for (const inputElement of this._elements) {
if (inputElement.IsValid(t)) {
return true
}
}
return false
}
GetValue(): UIEventSource<T> {
return this.value
}
protected InnerConstructElement(): HTMLElement {
const elements = this._elements
const selectFirstAsDefault = this._selectFirstAsDefault
const selectedElementIndex: UIEventSource<number> = new UIEventSource<number>(null)
const value = UIEventSource.flatten(
selectedElementIndex.map((selectedIndex) => {
if (selectedIndex !== undefined && selectedIndex !== null) {
return elements[selectedIndex].GetValue()
}
}),
elements.map((e) => e?.GetValue())
)
value.syncWith(this.value)
if (selectFirstAsDefault) {
value.addCallbackAndRun((selected) => {
if (selected === undefined) {
for (const element of elements) {
const v = element.GetValue().data
if (v !== undefined) {
value.setData(v)
break
}
}
}
})
}
for (let i = 0; i < elements.length; i++) {
// If an element is clicked, the radio button corresponding with it should be selected as well
elements[i]?.onClick(() => {
selectedElementIndex.setData(i)
})
elements[i].GetValue().addCallback(() => {
selectedElementIndex.setData(i)
})
}
const groupId = "radiogroup" + RadioButton._nextId
RadioButton._nextId++
const form = document.createElement("form")
const inputs = []
const wrappers: HTMLElement[] = []
for (let i1 = 0; i1 < elements.length; i1++) {
let element = elements[i1]
const labelHtml = element.ConstructElement()
if (labelHtml === undefined) {
continue
}
const input = document.createElement("input")
input.id = "radio" + groupId + "-" + i1
input.name = groupId
input.type = "radio"
input.classList.add("cursor-pointer", "p-1", "mr-2")
if (!this._dontStyle) {
input.classList.add("p-1", "ml-2", "pl-2", "pr-0", "m-3", "mr-0")
}
input.onchange = () => {
if (input.checked) {
selectedElementIndex.setData(i1)
}
}
inputs.push(input)
const label = document.createElement("label")
label.appendChild(labelHtml)
label.htmlFor = input.id
label.classList.add("flex", "w-full", "cursor-pointer")
if (!this._dontStyle) {
labelHtml.classList.add("p-2")
}
const block = document.createElement("div")
block.appendChild(input)
block.appendChild(label)
block.classList.add("flex", "w-full")
if (!this._dontStyle) {
block.classList.add("m-1", "border", "border-gray-400")
}
block.style.borderRadius = "1.5rem"
wrappers.push(block)
form.appendChild(block)
}
value.addCallbackAndRun((selected: T) => {
let somethingChecked = false
for (let i = 0; i < inputs.length; i++) {
let input = inputs[i]
input.checked = !somethingChecked && elements[i].IsValid(selected)
somethingChecked = somethingChecked || input.checked
if (input.checked) {
wrappers[i].classList.remove("border-gray-400")
wrappers[i].classList.add("border-attention")
} else {
wrappers[i].classList.add("border-gray-400")
wrappers[i].classList.remove("border-attention")
}
}
})
this.SetClass("flex flex-col")
return form
}
}

View file

@ -5,8 +5,8 @@
import { UIEventSource } from "../../../Logic/UIEventSource"
import BasicTagInput from "../../Studio/TagInput/BasicTagInput.svelte"
import { TagUtils } from "../../../Logic/Tags/TagUtils"
import nmd from "nano-markdown"
import FromHtml from "../../Base/FromHtml.svelte"
import Markdown from "../../Base/Markdown.svelte"
export let value: UIEventSource<undefined | string>
export let args: string[] = []
let uploadableOnly: boolean = args[0] === "uploadableOnly"
@ -34,6 +34,6 @@
{#if $dropdownFocussed}
<div class="m-2 border border-dashed border-black p-2">
<b>{documentation.name}</b>
<FromHtml src={nmd(documentation.docs)} />
<Markdown src={documentation.docs} />
</div>
{/if}

View file

@ -91,11 +91,6 @@
return
}
if (unit !== undefined && isNaN(Number(v))) {
value.setData(undefined)
return
}
feedback?.setData(undefined)
if (selectedUnit.data) {
value.setData(unit.toOsm(v, selectedUnit.data))

View file

@ -28,6 +28,7 @@ import TagValidator from "./Validators/TagValidator"
import IdValidator from "./Validators/IdValidator"
import SlopeValidator from "./Validators/SlopeValidator"
import VeloparkValidator from "./Validators/VeloparkValidator"
import CurrencyValidator from "./Validators/CurrencyValidator"
export type ValidatorType = (typeof Validators.availableTypes)[number]
@ -60,6 +61,7 @@ export default class Validators {
"id",
"slope",
"velopark",
"currency"
] as const
public static readonly AllValidators: ReadonlyArray<Validator> = [
@ -89,6 +91,7 @@ export default class Validators {
new IdValidator(),
new SlopeValidator(),
new VeloparkValidator(),
new CurrencyValidator()
]
private static _byType = Validators._byTypeConstructor()

View file

@ -0,0 +1,73 @@
import { Validator } from "../Validator"
import { Utils } from "../../../Utils"
export default class CurrencyValidator extends Validator {
private readonly segmenter: Intl.Segmenter
private readonly symbolToCurrencyMapping: Map<string, string>
private readonly supportedCurrencies: Set<string>
constructor() {
super("currency", "Validates monetary amounts")
if (Intl.Segmenter === undefined || Utils.runningFromConsole) {
// Librewolf doesn't support this
return
}
let locale = "en-US"
if(!Utils.runningFromConsole){
locale??= navigator.language
}
this.segmenter = new Intl.Segmenter(locale, {
granularity: "word"
})
const mapping: Map<string, string> = new Map<string, string>()
const supportedCurrencies: Set<string> = new Set(Intl.supportedValuesOf("currency"))
this.supportedCurrencies = supportedCurrencies
for (const currency of supportedCurrencies) {
const symbol = (0).toLocaleString(
locale,
{
style: "currency",
currency: currency,
minimumFractionDigits: 0,
maximumFractionDigits: 0
}
).replace(/\d/g, "").trim()
mapping.set(symbol.toLowerCase(), currency)
}
this.symbolToCurrencyMapping = mapping
}
reformat(s: string): string {
if (!this.segmenter) {
return s
}
const parts = Array.from(this.segmenter.segment(s)).map(i => i.segment).filter(part => part.trim().length > 0)
if(parts.length !== 2){
return s
}
const mapping = this.symbolToCurrencyMapping
let currency: string = undefined
let amount = undefined
for (const part of parts) {
const lc = part.toLowerCase()
if (this.supportedCurrencies.has(part.toUpperCase())) {
currency = part.toUpperCase()
continue
}
if (mapping.has(lc)) {
currency = mapping.get(lc)
continue
}
amount = part
}
if(amount === undefined || currency === undefined){
return s
}
return amount+" "+currency
}
}

View file

@ -3,7 +3,7 @@ import UrlValidator from "./UrlValidator"
export default class VeloparkValidator extends UrlValidator {
constructor() {
super("velopark", "A custom element to allow copy-pasting velopark-pages")
super("velopark", "A special URL-validator that checks the domain name and rewrites to the correct velopark format.")
}
getFeedback(s: string): Translation {

View file

@ -34,6 +34,7 @@
import { LinkIcon } from "@babeard/svelte-heroicons/mini"
import Square_rounded from "../../assets/svg/Square_rounded.svelte"
import Bug from "../../assets/svg/Bug.svelte"
import Pop_out from "../../assets/svg/Pop_out.svelte"
/**
* Renders a single icon.
@ -123,6 +124,9 @@
<AddSmall {color} class={clss} />
{:else if icon === "link"}
<LinkIcon style="--svg-color: {color}" class={twMerge(clss, "apply-fill")} />
{:else if icon === "popout"}
<LinkIcon style="--svg-color: {color}" />
{:else}
<img class={clss ?? "h-full w-full"} src={icon} aria-hidden="true" alt="" />
{/if}

View file

@ -38,11 +38,13 @@
{#if !allCalculatedTags.has(key)}
<tr>
<td>{key}</td>
<td>
<td style="width: 75%">
{#if $tags[key] === undefined}
<i>undefined</i>
{:else if $tags[key] === ""}
<i>Empty string</i>
{:else if typeof $tags[key] === "object"}
<div class="literal-code" >{JSON.stringify($tags[key])}</div>
{:else}
{$tags[key]}
{/if}

View file

@ -30,7 +30,8 @@
import { placeholder } from "../../../Utils/placeholder"
import { TrashIcon } from "@rgossiaux/svelte-heroicons/solid"
import { Tag } from "../../../Logic/Tags/Tag"
import { get, writable } from "svelte/store"
import { get } from "svelte/store"
import Markdown from "../../Base/Markdown.svelte"
export let config: TagRenderingConfig
export let tags: UIEventSource<Record<string, string>>
@ -68,13 +69,17 @@
/**
* Prepares and fills the checkedMappings
*/
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig) {
function initialize(tgs: Record<string, string>, confg: TagRenderingConfig): void {
mappings = confg.mappings?.filter((m) => {
if (typeof m.hideInAnswer === "boolean") {
return !m.hideInAnswer
}
return !m.hideInAnswer.matchesProperties(tgs)
})
selectedMapping = mappings?.findIndex(mapping => mapping.if.matchesProperties(tgs) || mapping.alsoShowIf?.matchesProperties(tgs))
if(selectedMapping < 0){
selectedMapping = undefined
}
// We received a new config -> reinit
unit = layer?.units?.find((unit) => unit.appliesToKeys.has(config.freeform?.key))
@ -85,7 +90,7 @@
checkedMappings?.length < confg.mappings.length + (confg.freeform ? 1 : 0))
) {
const seenFreeforms = []
// Initial setup of the mappings
// Initial setup of the mappings; detect checked mappings
checkedMappings = [
...confg.mappings.map((mapping) => {
if (mapping.hideInAnswer === true) {
@ -97,7 +102,7 @@
seenFreeforms.push(newProps[confg.freeform.key])
}
return matches
}),
})
]
if (tgs !== undefined && confg.freeform) {
@ -128,6 +133,8 @@
freeformInput.set(undefined)
}
feedback.setData(undefined)
}
$: {
@ -171,6 +178,9 @@
checkedMappings,
tags.data
)
if(state.featureSwitches.featureSwitchIsDebugging.data){
console.log("Constructing change spec from", {freeform: $freeformInput, selectedMapping, checkedMappings, currentTags: tags.data}, " --> ", selectedTags)
}
} catch (e) {
console.error("Could not calculate changeSpecification:", e)
selectedTags = undefined
@ -203,7 +213,7 @@
dispatch("saved", { config, applied: selectedTags })
const change = new ChangeTagAction(tags.data.id, selectedTags, tags.data, {
theme: tags.data["_orig_theme"] ?? state.layout.id,
changeType: "answer",
changeType: "answer"
})
freeformInput.set(undefined)
selectedMapping = undefined
@ -255,15 +265,19 @@
</div>
{#if config.questionhint}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{#if config.questionHintIsMd}
<Markdown srcWritable={ config.questionhint.current} />
{:else}
<div class="max-h-60 overflow-y-auto">
<SpecialTranslation
t={config.questionhint}
{tags}
{state}
{layer}
feature={selectedElement}
/>
</div>
{/if}
{/if}
</legend>

View file

@ -64,10 +64,14 @@
)
</script>
{#if unit.inverted}
<div class="bold px-2">/</div>
{/if}
<select bind:value={$selectedUnit}>
{#each unit.denominations as denom (denom.canonical)}
<option value={denom.canonical}>
{#if $isSingle}
{#if $isSingle || unit.inverted}
<Tr t={denom.humanSingular} />
{:else}
<Tr t={denom.human.Subs({ quantity: "" })} />

View file

@ -20,12 +20,16 @@
import NextButton from "../Base/NextButton.svelte"
import BackButton from "../Base/BackButton.svelte"
import DeleteButton from "./DeleteButton.svelte"
import StudioHashSetter from "./StudioHashSetter"
const layerSchema: ConfigMeta[] = <any>layerSchemaRaw
export let state: EditLayerState
export let backToStudio: () => void
new StudioHashSetter("layer", state.selectedTab, state.getStoreFor(["id"]))
let messages = state.messages
let hasErrors = messages.mapD(
(m: ConversionMessage[]) => m.filter((m) => m.level === "error").length
@ -127,14 +131,14 @@
{/each}
{:else}
<div class="m4 h-full overflow-y-auto">
<TabbedGroup>
<TabbedGroup tab={state.selectedTab}>
<div slot="title0" class="flex">
General properties
<ErrorIndicatorForRegion firstPaths={firstPathsFor("Basic")} {state} />
</div>
<div class="flex flex-col mb-8" slot="content0">
<Region {state} configs={perRegion["Basic"]} />
<DeleteButton {state} {backToStudio} objectType="layer"/>
<DeleteButton {state} {backToStudio} objectType="layer" />
</div>
<div slot="title1" class="flex">
@ -185,19 +189,18 @@
Below, you'll find the raw configuration file in `.json`-format. This is mostly for
debugging purposes, but you can also edit the file directly if you want.
</div>
<div class="literal-code overflow-y-auto h-full" style="min-height: 75%">
<RawEditor {state} />
</div>
<ShowConversionMessages messages={$messages} />
<div class="flex h-full w-full flex-row justify-between overflow-y-auto">
<div class="literal-code h-full w-5/6 overflow-y-auto">
<RawEditor {state} />
</div>
<div class="h-full w-1/6">
<div>
The testobject (which is used to render the questions in the 'information panel'
item has the following tags:
</div>
<AllTagsPanel tags={state.testTags} />
<div class="flex w-full flex-col">
<div>
The testobject (which is used to render the questions in the 'information panel'
item has the following tags:
</div>
<AllTagsPanel tags={state.testTags} />
</div>
</div>
</TabbedGroup>

View file

@ -42,6 +42,11 @@ export abstract class EditJsonState<T> {
public readonly configuration: UIEventSource<Partial<T>> = new UIEventSource<Partial<T>>({})
public readonly messages: Store<ConversionMessage[]>
/**
* The tab in the UI that is selected, used for deeplinks
*/
public readonly selectedTab: UIEventSource<number> = new UIEventSource<number>(0)
/**
* The EditLayerUI shows a 'schemaBasedInput' for this path to pop advanced questions out
*/
@ -508,6 +513,9 @@ export class EditThemeState extends EditJsonState<LayoutConfigJson> {
}
const prepare = this.buildValidation(state)
const context = ConversionContext.construct([], ["prepare"])
if(configuration.layers){
Utils.NoNullInplace(configuration.layers)
}
try {
prepare.convert(<LayoutConfigJson>configuration, context)
} catch (e) {

View file

@ -9,12 +9,15 @@
import RawEditor from "./RawEditor.svelte"
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
import DeleteButton from "./DeleteButton.svelte"
import { UIEventSource } from "../../Logic/UIEventSource"
import StudioHashSetter from "./StudioHashSetter"
export let state: EditThemeState
export let osmConnection: OsmConnection
export let backToStudio: () => void
let schema: ConfigMeta[] = state.schema.filter((schema) => schema.path.length > 0)
new StudioHashSetter("theme", state.selectedTab, state.getStoreFor(["id"]))
export let selfLayers: { owner: number; id: string }[]
export let otherLayers: { owner: number; id: string }[]
@ -94,7 +97,7 @@
<div class="m4 h-full overflow-y-auto">
<!-- {Object.keys(perRegion).join(";")} -->
<TabbedGroup>
<TabbedGroup tab={state.selectedTab}>
<div slot="title0">Basic properties</div>
<div slot="content0" class="mb-8">
<Region configs={perRegion["basic"]} path={[]} {state} title="Basic properties" />
@ -123,11 +126,10 @@
Below, you'll find the raw configuration file in `.json`-format. This is mostly for
debugging purposes, but you can also edit the file directly if you want.
</div>
<ShowConversionMessages messages={$messages} />
<div class="literal-code h-full w-full">
<div class="literal-code overflow-y-auto h-full" style="min-height: 75%">
<RawEditor {state} />
</div>
</div>
<ShowConversionMessages messages={$messages} />
</TabbedGroup>
</div>
</div>

View file

@ -5,12 +5,12 @@
import { ImmutableStore, Store } from "../../Logic/UIEventSource"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import nmd from "nano-markdown"
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson.js"
import type { TagRenderingConfigJson } from "../../Models/ThemeConfig/Json/TagRenderingConfigJson"
import FromHtml from "../Base/FromHtml.svelte"
import ShowConversionMessage from "./ShowConversionMessage.svelte"
import NextButton from "../Base/NextButton.svelte"
import Markdown from "../Base/Markdown.svelte"
export let state: EditLayerState
export let path: ReadonlyArray<string | number>
@ -62,13 +62,6 @@
let messages = state.messagesFor(path)
let description = schema.description
if (description) {
try {
description = nmd(description)
} catch (e) {
console.error("Could not convert description to markdown", { description })
}
}
</script>
<div class="flex">
@ -82,7 +75,7 @@
{/if}
</NextButton>
{#if description}
<FromHtml src={description} />
<Markdown src={description}/>
{/if}
{#each $messages as message}
<ShowConversionMessage {message} />

View file

@ -8,6 +8,7 @@
import QuestionPreview from "./QuestionPreview.svelte"
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte"
import ShowConversionMessage from "./ShowConversionMessage.svelte"
import Markdown from "../Base/Markdown.svelte"
export let state: EditLayerState
export let schema: ConfigMeta
@ -30,10 +31,9 @@
schema.description = undefined
}
const subparts: ConfigMeta = state
const subparts: ConfigMeta[] = state
.getSchemaStartingWith(schema.path)
.filter((part) => part.path.length - 1 === schema.path.length)
let messages = state.messagesFor(path)
const currentValue: UIEventSource<any[]> = state.getStoreFor(path)
@ -97,9 +97,7 @@
<h3>{schema.path.at(-1)}</h3>
{#if subparts.length > 0}
<span class="subtle">
{schema.description}
</span>
<Markdown src={schema.description}/>
{/if}
{#if $currentValue === undefined}
No array defined

View file

@ -3,7 +3,6 @@
import type { ConfigMeta } from "./configMeta"
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte"
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
import nmd from "nano-markdown"
import type { QuestionableTagRenderingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson"
import EditLayerState from "./EditLayerState"
import { onDestroy } from "svelte"
@ -68,11 +67,12 @@
type = type.substring(0, type.length - 2)
}
const configJson: QuestionableTagRenderingConfigJson = {
const configJson: QuestionableTagRenderingConfigJson & {questionHintIsMd: boolean} = {
id: path.join("_"),
render: rendervalue,
question: schema.hints.question,
questionHint: nmd(schema.description),
questionHint: schema.description,
questionHintIsMd: true,
freeform:
schema.type === "boolean"
? undefined

View file

@ -8,9 +8,8 @@
import { onDestroy } from "svelte"
import SchemaBasedInput from "./SchemaBasedInput.svelte"
import type { JsonSchemaType } from "./jsonSchema"
// @ts-ignore
import nmd from "nano-markdown"
import ShowConversionMessage from "./ShowConversionMessage.svelte"
import type { Translatable } from "../../Models/ThemeConfig/Json/Translatable"
/**
* If 'types' is defined: allow the user to pick one of the types to input.
@ -41,10 +40,11 @@
if (lastIsString) {
types.splice(types.length - 1, 1)
}
const configJson: QuestionableTagRenderingConfigJson = {
const configJson: QuestionableTagRenderingConfigJson & {questionHintIsMd: boolean}= {
id: "TYPE_OF:" + path.join("_"),
question: "Which subcategory is needed for " + schema.path.at(-1) + "?",
questionHint: nmd(schema.description),
question: schema.hints.question ?? "Which subcategory is needed for " + schema.path.at(-1) + "?",
questionHint: schema.description,
questionHintIsMd: true,
mappings: types
.map((opt) => opt.trim())
.filter((opt) => opt.length > 0)

View file

@ -0,0 +1,11 @@
import { Store, UIEventSource } from "../../Logic/UIEventSource"
import Hash from "../../Logic/Web/Hash"
export default class StudioHashSetter {
constructor(mode: "layer" | "theme", tab: Store<number>, name: Store<string>) {
tab.mapD(tab => {
Hash.hash.setData(mode + "/" + name.data + "/" + tab)
}
, [name])
}
}

View file

@ -1,7 +1,7 @@
<script lang="ts">
import NextButton from "./Base/NextButton.svelte"
import { Store, UIEventSource } from "../Logic/UIEventSource"
import EditLayerState, { EditThemeState } from "./Studio/EditLayerState"
import EditLayerState, { EditJsonState, EditThemeState } from "./Studio/EditLayerState"
import EditLayer from "./Studio/EditLayer.svelte"
import Loading from "../assets/svg/Loading.svelte"
import StudioServer from "./Studio/StudioServer"
@ -30,6 +30,7 @@
import Tr from "./Base/Tr.svelte"
import Add from "../assets/svg/Add.svelte"
import { SearchIcon } from "@rgossiaux/svelte-heroicons/solid"
import Hash from "../Logic/Web/Hash"
export let studioUrl =
window.location.hostname === "127.0.0.2"
@ -43,11 +44,11 @@
)
let osmConnection = new OsmConnection({
oauth_token,
checkOnlineRegularly: true,
checkOnlineRegularly: true
})
const expertMode = UIEventSource.asBoolean(
osmConnection.GetPreference("studio-expert-mode", "false", {
documentation: "Indicates if more options are shown in mapcomplete studio",
documentation: "Indicates if more options are shown in mapcomplete studio"
})
)
expertMode.addCallbackAndRunD((expert) => console.log("Expert mode is", expert))
@ -61,12 +62,12 @@
l["success"]?.filter((l) => l.category === "layers")
)
$: selfLayers = layers.mapD(
(ls) =>
ls.filter(
(l) => l.owner === uid.data && l.id.toLowerCase().includes(layerFilterTerm.toLowerCase())
),
[uid]
)
(ls) =>
ls.filter(
(l) => l.owner === uid.data && l.id.toLowerCase().includes(layerFilterTerm.toLowerCase())
),
[uid]
)
$: otherLayers = layers.mapD(
(ls) =>
ls.filter(
@ -132,16 +133,17 @@
const version = meta.version
async function editLayer(event: Event) {
async function editLayer(event: { detail }): Promise<EditLayerState> {
const layerId: { owner: number; id: string } = event["detail"]
state = "loading"
editLayerState.startSavingUpdates(false)
editLayerState.configuration.setData(await studio.fetch(layerId.id, "layers", layerId.owner))
editLayerState.startSavingUpdates()
state = "editing_layer"
return editLayerState
}
async function editTheme(event: Event) {
async function editTheme(event: { detail }): Promise<EditThemeState> {
const id: { id: string; owner: number } = event["detail"]
state = "loading"
editThemeState.startSavingUpdates(false)
@ -149,6 +151,7 @@
editThemeState.configuration.setData(layout)
editThemeState.startSavingUpdates()
state = "editing_theme"
return editThemeState
}
async function createNewLayer() {
@ -162,23 +165,50 @@
marker: [
{
icon: "circle",
color: "white",
},
],
},
color: "white"
}
]
}
],
tagRenderings: ["images"],
lineRendering: [
{
width: 1,
color: "blue",
},
],
color: "blue"
}
]
}
editLayerState.configuration.setData(initialLayerConfig)
editLayerState.startSavingUpdates()
state = "editing_layer"
}
async function selectStateBasedOnHash() {
const hash = Hash.hash.data
if (!hash) {
return
}
console.log("Selecting state based on ", hash)
const [mode, id, tab] = hash.split("/")
// Not really an event, we just set the 'detail'
const event = {
detail: {
id,
owner: uid.data
}
}
const statePromise: Promise<EditJsonState<any>> = mode === "layer" ? editLayer(event) : editTheme(event)
const state = await statePromise
state.selectedTab.setData(Number(tab))
}
selectStateBasedOnHash()
function backToStudio() {
console.log("Back to studio")
state = undefined
Hash.hash.setData(undefined)
}
</script>
<If condition={layersWithErr.map((d) => d?.["error"] !== undefined)}>
@ -191,8 +221,8 @@
<li>Try again in a few minutes</li>
<li>
Contact <a href="https://app.element.io/#/room/#MapComplete:matrix.org">
the MapComplete community via the chat.
</a>
the MapComplete community via the chat.
</a>
Someone might be able to help you
</li>
<li>
@ -257,9 +287,7 @@
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => {
state = undefined
}}
on:click={() => backToStudio()}
>
MapComplete Studio
</BackButton>
@ -306,9 +334,7 @@
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => {
state = undefined
}}
on:click={() => backToStudio()}
>
MapComplete Studio
</BackButton>
@ -348,30 +374,23 @@
{:else if state === "editing_layer"}
<EditLayer
state={editLayerState}
backToStudio={() => {
state = undefined
}}
{backToStudio}
>
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => {
state = undefined
}}
on:click={() => backToStudio()}
>
MapComplete Studio
</BackButton>
</EditLayer>
{:else if state === "editing_theme"}
<EditTheme state={editThemeState} selfLayers={$selfLayers} otherLayers={$otherLayers} {osmConnection} backToStudio={() => {
state = undefined
}}>
<EditTheme state={editThemeState} selfLayers={$selfLayers} otherLayers={$otherLayers} {osmConnection}
{backToStudio}>
<BackButton
clss="small p-1"
imageClass="w-8 h-8"
on:click={() => {
state = undefined
}}
on:click={() => backToStudio()}
>
MapComplete Studio
</BackButton>

View file

@ -34,7 +34,6 @@
import { Utils } from "../Utils"
import Hotkeys from "./Base/Hotkeys"
import LevelSelector from "./BigComponents/LevelSelector.svelte"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton"
import SelectedElementTitle from "./BigComponents/SelectedElementTitle.svelte"
import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"
import type { RasterLayerPolygon } from "../Models/RasterLayers"
@ -73,6 +72,7 @@
import PrivacyPolicy from "./BigComponents/PrivacyPolicy.svelte"
import { BBox } from "../Logic/BBox"
import ReviewsOverview from "./Reviews/ReviewsOverview.svelte"
import ExtraLinkButton from "./BigComponents/ExtraLinkButton.svelte"
export let state: ThemeViewState
let layout = state.layout
@ -260,9 +260,7 @@
/>
</MapControlButton>
{/if}
<ToSvelte
construct={() => new ExtraLinkButton(state, layout.extraLink).SetClass("pointer-events-auto")}
/>
<ExtraLinkButton {state} />
<UploadingImageCounter featureId="*" showThankYou={false} {state} />
<PendingChangesIndicator {state} />
<If condition={state.featureSwitchIsTesting}>
@ -285,9 +283,9 @@
<div class="flex w-full items-end justify-between px-4">
<div class="flex flex-col">
<If condition={featureSwitches.featureSwitchEnableLogin}>
{#if state.layout.hasPresets() || state.layout.hasNoteLayer()}
{#if (state.layout.hasPresets() && state.layout.enableAddNewPoints) || state.layout.hasNoteLayer()}
<button
class="pointer-events-auto w-fit"
class="pointer-events-auto w-fit low-interaction"
class:disabled={$currentZoom < Constants.minZoomLevelToAddNewPoint}
on:click={() => {
state.openNewDialog()

View file

@ -1,8 +1,8 @@
<script lang="ts">
import nmd from "nano-markdown"
import { createEventDispatcher } from "svelte"
import WalkthroughStep from "./WalkthroughStep.svelte"
import FromHtml from "../Base/FromHtml.svelte"
import Markdown from "../Base/Markdown.svelte"
/**
* Markdown
@ -31,5 +31,5 @@
totalPages={pages.length}
pageNumber={currentPage}
>
<FromHtml src={nmd(pages[currentPage])} />
<Markdown src={pages[currentPage]} />
</WalkthroughStep>

View file

@ -11,7 +11,7 @@
</script>
<div class="link-underline flex h-full w-full flex-col justify-between">
<div class="overflow-y-auto">
<div class="overflow-y-auto h-full">
<slot />
</div>