forked from MapComplete/MapComplete
Studio: WIP
This commit is contained in:
parent
04ecdad1bb
commit
903e168a89
62 changed files with 19152 additions and 123399 deletions
20
src/UI/InputElement/Helpers/SimpleTagInput.svelte
Normal file
20
src/UI/InputElement/Helpers/SimpleTagInput.svelte
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<script lang="ts">/**
|
||||
* Input helper to create a tag. The tag is JSON-encoded
|
||||
*/
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import BasicTagInput from "../../Studio/TagInput/BasicTagInput.svelte";
|
||||
|
||||
export let value: UIEventSource<undefined | string>;
|
||||
export let uploadableOnly: boolean;
|
||||
export let overpassSupportNeeded: boolean;
|
||||
|
||||
/**
|
||||
* Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries)
|
||||
*/
|
||||
export let silent: boolean = false;
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<BasicTagInput {overpassSupportNeeded} {silent} tag={value} {uploadableOnly} />
|
||||
33
src/UI/InputElement/Helpers/TagInput.svelte
Normal file
33
src/UI/InputElement/Helpers/TagInput.svelte
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<script lang="ts">/**
|
||||
* Input helper to create a tag. The tag is JSON-encoded
|
||||
*/
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import FullTagInput from "../../Studio/TagInput/FullTagInput.svelte";
|
||||
|
||||
export let value: UIEventSource<undefined | string>;
|
||||
export let uploadableOnly: boolean;
|
||||
export let overpassSupportNeeded: boolean;
|
||||
|
||||
/**
|
||||
* Only show the taginfo-statistics if they are suspicious (thus: less then 250 entries)
|
||||
*/
|
||||
export let silent: boolean = false;
|
||||
|
||||
let tag: UIEventSource<string | TagConfigJson> = value.sync(s => {
|
||||
try {
|
||||
return JSON.parse(s);
|
||||
} catch (e) {
|
||||
return s;
|
||||
}
|
||||
}, [], t => {
|
||||
if(typeof t === "string"){
|
||||
return t
|
||||
}
|
||||
return JSON.stringify(t);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<FullTagInput {overpassSupportNeeded} {silent} {tag} {uploadableOnly} />
|
||||
|
|
@ -15,6 +15,8 @@ import { Feature } from "geojson"
|
|||
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||
import ImageHelper from "./Helpers/ImageHelper.svelte"
|
||||
import TranslationInput from "./Helpers/TranslationInput.svelte"
|
||||
import TagInput from "./Helpers/TagInput.svelte"
|
||||
import SimpleTagInput from "./Helpers/SimpleTagInput.svelte"
|
||||
|
||||
export interface InputHelperProperties {
|
||||
/**
|
||||
|
|
@ -59,9 +61,11 @@ export default class InputHelpers {
|
|||
wikidata: InputHelpers.constructWikidataHelper,
|
||||
image: (value) => new SvelteUIElement(ImageHelper, { value }),
|
||||
translation: (value) => new SvelteUIElement(TranslationInput, { value }),
|
||||
tag: (value) => new SvelteUIElement(TagInput, { value }),
|
||||
simple_tag: (value) => new SvelteUIElement(SimpleTagInput, { value }),
|
||||
} as const
|
||||
|
||||
public static hideInputField : string[] = ["translation"]
|
||||
public static hideInputField: string[] = ["translation", "simple_tag", "tag"]
|
||||
|
||||
/**
|
||||
* Constructs a mapProperties-object for the given properties.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import BaseUIElement from "../BaseUIElement";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import Translations from "../i18n/Translations";
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Translation } from "../i18n/Translation"
|
||||
import Translations from "../i18n/Translations"
|
||||
|
||||
/**
|
||||
* A 'TextFieldValidator' contains various methods to check and cleanup an entered value or to give feedback.
|
||||
|
|
@ -16,13 +16,23 @@ export abstract class Validator {
|
|||
/**
|
||||
* What HTML-inputmode to use
|
||||
*/
|
||||
public readonly inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'
|
||||
public readonly inputmode?:
|
||||
| "none"
|
||||
| "text"
|
||||
| "tel"
|
||||
| "url"
|
||||
| "email"
|
||||
| "numeric"
|
||||
| "decimal"
|
||||
| "search"
|
||||
public readonly textArea: boolean
|
||||
|
||||
public readonly isMeta?: boolean
|
||||
|
||||
constructor(
|
||||
name: string,
|
||||
explanation: string | BaseUIElement,
|
||||
inputmode?: 'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search',
|
||||
inputmode?: "none" | "text" | "tel" | "url" | "email" | "numeric" | "decimal" | "search",
|
||||
textArea?: false | boolean
|
||||
) {
|
||||
this.name = name
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import ImageUrlValidator from "./Validators/ImageUrlValidator"
|
|||
import TagKeyValidator from "./Validators/TagKeyValidator"
|
||||
import TranslationValidator from "./Validators/TranslationValidator"
|
||||
import FediverseValidator from "./Validators/FediverseValidator"
|
||||
import IconValidator from "./Validators/IconValidator"
|
||||
import TagValidator from "./Validators/TagValidator"
|
||||
|
||||
export type ValidatorType = (typeof Validators.availableTypes)[number]
|
||||
|
||||
|
|
@ -48,7 +50,9 @@ export default class Validators {
|
|||
"simple_tag",
|
||||
"key",
|
||||
"translation",
|
||||
"icon",
|
||||
"fediverse",
|
||||
"tag",
|
||||
] as const
|
||||
|
||||
public static readonly AllValidators: ReadonlyArray<Validator> = [
|
||||
|
|
@ -70,20 +74,15 @@ export default class Validators {
|
|||
new ColorValidator(),
|
||||
new ImageUrlValidator(),
|
||||
new SimpleTagValidator(),
|
||||
new TagValidator(),
|
||||
new TagKeyValidator(),
|
||||
new TranslationValidator(),
|
||||
new IconValidator(),
|
||||
new FediverseValidator(),
|
||||
]
|
||||
|
||||
private static _byType = Validators._byTypeConstructor()
|
||||
|
||||
private static _byTypeConstructor(): Map<ValidatorType, Validator> {
|
||||
const map = new Map<ValidatorType, Validator>()
|
||||
for (const validator of Validators.AllValidators) {
|
||||
map.set(<ValidatorType>validator.name, validator)
|
||||
}
|
||||
return map
|
||||
}
|
||||
public static HelpText(): BaseUIElement {
|
||||
const explanations: BaseUIElement[] = Validators.AllValidators.map((type) =>
|
||||
new Combine([new Title(type.name, 3), type.explanation]).SetClass("flex flex-col")
|
||||
|
|
@ -95,6 +94,14 @@ export default class Validators {
|
|||
]).SetClass("flex flex-col")
|
||||
}
|
||||
|
||||
private static _byTypeConstructor(): Map<ValidatorType, Validator> {
|
||||
const map = new Map<ValidatorType, Validator>()
|
||||
for (const validator of Validators.AllValidators) {
|
||||
map.set(<ValidatorType>validator.name, validator)
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
static get(type: ValidatorType): Validator {
|
||||
return Validators._byType.get(type)
|
||||
}
|
||||
|
|
|
|||
46
src/UI/InputElement/Validators/IconValidator.ts
Normal file
46
src/UI/InputElement/Validators/IconValidator.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { Validator } from "../Validator"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import licenses from "../../../assets/generated/license_info.json"
|
||||
import { Utils } from "../../../Utils"
|
||||
|
||||
export default class IconValidator extends Validator {
|
||||
private static allLicenses = new Set(licenses.map((l) => l.path))
|
||||
private static allLicensesArr = Array.from(IconValidator.allLicenses)
|
||||
public static readonly isMeta = true
|
||||
constructor() {
|
||||
super("icon", "Makes sure that a valid .svg-path is added")
|
||||
}
|
||||
|
||||
getFeedback(s: string, getCountry, sloppy?: boolean): Translation | undefined {
|
||||
if (!s.startsWith("http")) {
|
||||
if (!IconValidator.allLicenses.has(s)) {
|
||||
const close = sloppy
|
||||
? []
|
||||
: Utils.sortedByLevenshteinDistance(
|
||||
s.substring(s.lastIndexOf("/")),
|
||||
IconValidator.allLicensesArr,
|
||||
(s) => s.substring(s.lastIndexOf("/"))
|
||||
).slice(0, 5)
|
||||
return new Translation(
|
||||
[
|
||||
`Unkown builtin icon ${s}, perhaps you meant one of: <ul>`,
|
||||
...close.map(
|
||||
(item) =>
|
||||
`<li><span class="flex justify-start"> <img src='${item}' class="w-6 h-6"/>${item}</span></li>`
|
||||
),
|
||||
"</ul>",
|
||||
].join("")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.endsWith(".svg")) {
|
||||
return new Translation("An icon should end with `.svg`")
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
isValid(key: string, getCountry?: () => string): boolean {
|
||||
return this.getFeedback(key, getCountry, true) === undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import { Translation } from "../../i18n/Translation"
|
|||
|
||||
export default class ImageUrlValidator extends UrlValidator {
|
||||
private static readonly allowedExtensions = ["jpg", "jpeg", "svg", "png"]
|
||||
public readonly isMeta = true
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import TagKeyValidator from "./TagKeyValidator"
|
|||
*/
|
||||
export default class SimpleTagValidator extends Validator {
|
||||
private static readonly KeyValidator = new TagKeyValidator()
|
||||
|
||||
public readonly isMeta = true
|
||||
constructor() {
|
||||
super(
|
||||
"simple_tag",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { Translation } from "../../i18n/Translation"
|
|||
import Translations from "../../i18n/Translations"
|
||||
|
||||
export default class TagKeyValidator extends Validator {
|
||||
|
||||
public readonly isMeta = true
|
||||
constructor() {
|
||||
super("key", "Validates a key, mostly that no weird characters are used")
|
||||
}
|
||||
|
|
|
|||
24
src/UI/InputElement/Validators/TagValidator.ts
Normal file
24
src/UI/InputElement/Validators/TagValidator.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { Validator } from "../Validator"
|
||||
import { Translation } from "../../i18n/Translation"
|
||||
import Translations from "../../i18n/Translations"
|
||||
import TagKeyValidator from "./TagKeyValidator"
|
||||
import SimpleTagValidator from "./SimpleTagValidator"
|
||||
|
||||
/**
|
||||
* Checks that the input conforms a JSON-encoded tag expression or a simpleTag`key=value`,
|
||||
*/
|
||||
export default class TagValidator extends Validator {
|
||||
|
||||
public readonly isMeta = true
|
||||
constructor() {
|
||||
super("tag", "A simple tag of the format `key=value` OR a tagExpression")
|
||||
}
|
||||
|
||||
getFeedback(tag: string, _): Translation | undefined {
|
||||
return undefined
|
||||
}
|
||||
|
||||
isValid(tag: string, _): boolean {
|
||||
return this.getFeedback(tag, _) === undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
import { Validator } from "../Validator"
|
||||
|
||||
export default class TranslationValidator extends Validator {
|
||||
|
||||
public readonly isMeta = true
|
||||
constructor() {
|
||||
super("translation", "Makes sure the the string is of format `Record<string, string>` ")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
})
|
||||
)
|
||||
|
||||
let htmlElem: HTMLBaseElement
|
||||
let htmlElem: HTMLDivElement
|
||||
$: {
|
||||
if (editMode && htmlElem !== undefined) {
|
||||
// EditMode switched to true, so the person wants to make a change
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
|
||||
// Some delay is applied to give Svelte the time to render the _question_
|
||||
window.setTimeout(() => {
|
||||
Utils.scrollIntoView(htmlElem)
|
||||
Utils.scrollIntoView(<any> htmlElem)
|
||||
}, 50)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import layerSchemaRaw from "../../assets/layerconfigmeta.json"
|
||||
import layerSchemaRaw from "../../assets/schemas/layerconfigmeta.json"
|
||||
import Region from "./Region.svelte";
|
||||
import TabbedGroup from "../Base/TabbedGroup.svelte";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
|
|
@ -10,7 +10,7 @@
|
|||
|
||||
import drinking_water from "../../../assets/layers/drinking_water/drinking_water.json"
|
||||
|
||||
const layerSchema: ConfigMeta[] = layerSchemaRaw
|
||||
const layerSchema: ConfigMeta[] = <any> layerSchemaRaw
|
||||
let state = new EditLayerState(layerSchema)
|
||||
state.configuration.setData(drinking_water)
|
||||
/**
|
||||
|
|
@ -40,14 +40,13 @@
|
|||
<TabbedGroup tab={new UIEventSource(1)}>
|
||||
<div slot="title0">General properties</div>
|
||||
<div class="flex flex-col" slot="content0">
|
||||
<!--
|
||||
{#each baselayerRegions as region}
|
||||
<Region {state} configs={perRegion[region]} title={region}/>
|
||||
{/each}
|
||||
|
||||
{#each leftoverRegions as region}
|
||||
<Region {state} configs={perRegion[region]} title={region}/>
|
||||
{/each}-->
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div slot="title1">Information panel (questions and answers)</div>
|
||||
|
|
@ -57,9 +56,8 @@
|
|||
The bulk of the popup content
|
||||
</div>
|
||||
</Region>
|
||||
<!--
|
||||
<Region {state} configs={perRegion["title"]} title="Popup title"/>
|
||||
<Region {state} configs={perRegion["editing"]} title="Other editing elements"/>-->
|
||||
<Region {state} configs={perRegion["editing"]} title="Other editing elements"/>
|
||||
</div>
|
||||
|
||||
<div slot="title2">Rendering on the map</div>
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
|||
import { ConfigMeta } from "./configMeta"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import { LayerConfigJson } from "../../Models/ThemeConfig/Json/LayerConfigJson"
|
||||
import { DeleteConfigJson } from "../../Models/ThemeConfig/Json/DeleteConfigJson"
|
||||
import { Utils } from "../../Utils"
|
||||
|
||||
export default class EditLayerState {
|
||||
public readonly osmConnection: OsmConnection
|
||||
|
|
@ -40,6 +38,15 @@ export default class EditLayerState {
|
|||
return entry
|
||||
}
|
||||
|
||||
public getStoreFor(path: ReadonlyArray<string | number>): UIEventSource<any | undefined> {
|
||||
const store = new UIEventSource<any>(this.getCurrentValueFor(path))
|
||||
store.addCallback((v) => {
|
||||
console.log("UPdating store", path, v)
|
||||
this.setValueAt(path, v)
|
||||
})
|
||||
return store
|
||||
}
|
||||
|
||||
public register(
|
||||
path: ReadonlyArray<string | number>,
|
||||
value: Store<any>,
|
||||
|
|
|
|||
71
src/UI/Studio/MappingInput.svelte
Normal file
71
src/UI/Studio/MappingInput.svelte
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type { MappingConfigJson } from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils";
|
||||
import FromHtml from "../Base/FromHtml.svelte";
|
||||
import { PencilIcon } from "@rgossiaux/svelte-heroicons/outline";
|
||||
import Region from "./Region.svelte";
|
||||
import type { ConfigMeta } from "./configMeta";
|
||||
import configs from "../../assets/schemas/questionabletagrenderingconfigmeta.json";
|
||||
import { Utils } from "../../Utils";
|
||||
|
||||
export let mapping: MappingConfigJson;
|
||||
export let state: EditLayerState;
|
||||
export let path: (string | number)[];
|
||||
let tag: UIEventSource<TagConfigJson> = state.getStoreFor([...path, "if"]);
|
||||
let parsedTag = tag.map(t => t ? TagUtils.Tag(t) : undefined);
|
||||
let exampleTags = parsedTag.map(pt => {
|
||||
if (!pt) {
|
||||
return {};
|
||||
}
|
||||
const keys = pt.usedKeys();
|
||||
const o = {};
|
||||
for (const key of keys) {
|
||||
o[key] = "value";
|
||||
}
|
||||
return o;
|
||||
});
|
||||
let uploadableOnly: boolean = true;
|
||||
|
||||
let thenStringified = state.getStoreFor([...path, "then"]).sync(t => t ? JSON.stringify(t) : undefined, [], s => s ? JSON.parse(s) : undefined);
|
||||
let thenParsed = thenStringified.map(s => s ? JSON.parse(s) : s);
|
||||
let editMode = Object.keys(thenParsed.data).length === 0;
|
||||
|
||||
const mappingConfigs: ConfigMeta[] = configs.filter(c => c.path[0] === "mappings")
|
||||
.map(c => <ConfigMeta>Utils.Clone(c))
|
||||
.map(c => {
|
||||
c.path.splice(0, 1);
|
||||
return c;
|
||||
})
|
||||
.filter(c => c.path.length == 1 && c.hints.group !== "hidden");
|
||||
</script>
|
||||
|
||||
<button on:click={() => {editMode = !editMode}}>
|
||||
<PencilIcon class="w-6 h-6" />
|
||||
</button>
|
||||
|
||||
{#if editMode}
|
||||
<div class="flex justify-between items-start w-full">
|
||||
<div class="flex flex-col w-full">
|
||||
<Region {state} configs={mappingConfigs} path={path} />
|
||||
</div>
|
||||
|
||||
<slot name="delete" />
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
{#if Object.keys($thenParsed).length > 0}
|
||||
<b>
|
||||
{new Translation($thenParsed).txt}
|
||||
</b>
|
||||
{:else}
|
||||
<i>No then is set</i>
|
||||
{/if}
|
||||
<FromHtml src={ $parsedTag?.asHumanString(false, false, $exampleTags)} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
@ -2,28 +2,32 @@
|
|||
* A 'region' is a collection of properties that can be edited which are somewhat related.
|
||||
* They will typically be a subset of some properties
|
||||
*/
|
||||
import type {ConfigMeta} from "./configMeta";
|
||||
import type { ConfigMeta } from "./configMeta";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte";
|
||||
|
||||
export let state: EditLayerState
|
||||
export let configs: ConfigMeta[]
|
||||
export let title: string
|
||||
export let state: EditLayerState;
|
||||
export let configs: ConfigMeta[];
|
||||
export let title: string | undefined = undefined;
|
||||
|
||||
export let path: (string | number)[] = [];
|
||||
|
||||
</script>
|
||||
{#if title}
|
||||
<div class="w-full flex flex-col">
|
||||
<h3>{title}</h3>
|
||||
<div class="pl-2 border border-black flex flex-col gap-y-1">
|
||||
<slot name="description"/>
|
||||
{#each configs as config}
|
||||
<SchemaBasedInput {state} path={config.path} schema={config}/>
|
||||
{/each}
|
||||
<div class="pl-2 border border-black flex flex-col gap-y-1 w-full">
|
||||
<slot name="description" />
|
||||
{#each configs as config}
|
||||
<SchemaBasedInput {state} path={config.path} schema={config} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="pl-2 flex flex-col gap-y-1">
|
||||
{#each configs as config}
|
||||
<SchemaBasedInput {state} path={config.path} schema={config}/>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="pl-2 flex flex-col gap-y-1 w-full">
|
||||
{#each configs as config}
|
||||
<SchemaBasedInput {state} path={path.concat(config.path)} schema={config} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
import EditLayerState from "./EditLayerState";
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import TagInput from "./TagInput/TagInput.svelte";
|
||||
import FullTagInput from "./TagInput/FullTagInput.svelte";
|
||||
import type {ConfigMeta} from "./configMeta";
|
||||
import {PencilAltIcon} from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { onDestroy } from "svelte";
|
||||
|
|
@ -40,13 +40,14 @@
|
|||
<div class="interactive border-interactive">
|
||||
<h3>{schema.hints.question ?? "What tags should be applied?"}</h3>
|
||||
{schema.description}
|
||||
<TagInput {tag}/>
|
||||
<FullTagInput {tag}/>
|
||||
<div class="flex justify-end">
|
||||
|
||||
<button class="primary w-fit" on:click={() => {mode = "set"}}>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
<div class="subtle">RegisteredTagInput based on schema: {JSON.stringify(schema)}</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="low-interaction flex justify-between">
|
||||
|
|
|
|||
|
|
@ -10,16 +10,18 @@
|
|||
export let state: EditLayerState;
|
||||
export let schema: ConfigMeta;
|
||||
|
||||
|
||||
let title = schema.path.at(-1);
|
||||
let singular = title;
|
||||
if (title.endsWith("s")) {
|
||||
if (title?.endsWith("s")) {
|
||||
singular = title.slice(0, title.length - 1);
|
||||
}
|
||||
let article = "a";
|
||||
if (singular.match(/^[aeoui]/)) {
|
||||
if (singular?.match(/^[aeoui]/)) {
|
||||
article = "an";
|
||||
}
|
||||
export let path: (string | number)[] = [];
|
||||
const isTagRenderingBlock = path.length === 1 && path[0] === "tagRenderings";
|
||||
|
||||
const subparts = state.getSchemaStartingWith(schema.path);
|
||||
|
||||
|
|
@ -39,8 +41,11 @@
|
|||
let createdItems = values.data.length;
|
||||
|
||||
|
||||
function createItem() {
|
||||
function createItem(valueToSet?: any) {
|
||||
values.data.push(createdItems);
|
||||
if (valueToSet) {
|
||||
state.setValueAt([...path, createdItems], valueToSet);
|
||||
}
|
||||
createdItems++;
|
||||
values.ping();
|
||||
}
|
||||
|
|
@ -87,16 +92,24 @@
|
|||
{/each}
|
||||
{:else}
|
||||
{#each $values as value (value)}
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="m-0">{singular} {value}</h3>
|
||||
<button class="border-black border rounded-full p-1 w-fit h-fit"
|
||||
on:click={() => {del(value)}}>
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if !isTagRenderingBlock}
|
||||
<div class="flex justify-between items-center">
|
||||
<h3 class="m-0">{singular} {value}</h3>
|
||||
<button class="border-black border rounded-full p-1 w-fit h-fit"
|
||||
on:click={() => {del(value)}}>
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="border border-black">
|
||||
{#if path.length === 1 && path[0] === "tagRenderings"}
|
||||
<TagRenderingInput path={path.concat(value)} {state} {schema}/>
|
||||
{#if isTagRenderingBlock}
|
||||
<TagRenderingInput path={path.concat(value)} {state} {schema} >
|
||||
<button slot="upper-right" class="border-black border rounded-full p-1 w-fit h-fit"
|
||||
on:click={() => {del(value)}}>
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</TagRenderingInput>
|
||||
{:else}
|
||||
{#each subparts as subpart}
|
||||
<SchemaBasedInput {state} path={fusePath(value, subpart.path)} schema={subpart} />
|
||||
|
|
@ -105,5 +118,11 @@
|
|||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
<button on:click={createItem}>Add {article} {singular}</button>
|
||||
<div class="flex">
|
||||
<button on:click={() => createItem()}>Add {article} {singular}</button>
|
||||
{#if path.length === 1 && path[0] === "tagRenderings"}
|
||||
<button on:click={() => {createItem();}}>Add a builtin tagRendering</button>
|
||||
{/if}
|
||||
<slot name="extra-button" />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
} from "../../Models/ThemeConfig/Json/QuestionableTagRenderingConfigJson";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import { onDestroy } from "svelte";
|
||||
import type { JsonSchemaType } from "./jsonSchema";
|
||||
|
||||
|
||||
export let state: EditLayerState
|
||||
|
|
@ -21,6 +22,9 @@
|
|||
if(type === "rendered"){
|
||||
type = "translation"
|
||||
}
|
||||
if(type.endsWith("[]")){
|
||||
type = type.substring(0, type.length - 2)
|
||||
}
|
||||
const isTranslation =schema.hints.typehint === "translation" || schema.hints.typehint === "rendered"
|
||||
|
||||
const configJson: QuestionableTagRenderingConfigJson = {
|
||||
|
|
@ -47,7 +51,23 @@
|
|||
}]
|
||||
}
|
||||
|
||||
if (schema.type === "boolean" || (Array.isArray(schema.type) && schema.type.some(t => t["type"] === "boolean"))) {
|
||||
function mightBeBoolean(type: undefined | JsonSchemaType): boolean {
|
||||
if(type === undefined){
|
||||
return false
|
||||
}
|
||||
if(type["type"]){
|
||||
type = type["type"]
|
||||
}
|
||||
if(type === "boolean"){
|
||||
return true
|
||||
}
|
||||
if(!Array.isArray(type)){
|
||||
return false
|
||||
}
|
||||
|
||||
return type.some(t => mightBeBoolean(t) )
|
||||
}
|
||||
if (mightBeBoolean(schema.type)) {
|
||||
configJson.mappings = configJson.mappings ?? []
|
||||
configJson.mappings.push(
|
||||
{
|
||||
|
|
@ -101,7 +121,7 @@
|
|||
{#if err !== undefined}
|
||||
<span class="alert">{err}</span>
|
||||
{:else}
|
||||
<div class="w-full">
|
||||
<div class="w-full flex flex-col">
|
||||
<TagRenderingEditable {config} selectedElement={undefined} showQuestionIfUnknown={true} {state} {tags}/>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,31 +1,25 @@
|
|||
<script lang="ts">
|
||||
import type { ConfigMeta } from "./configMeta";
|
||||
import SchemaBasedField from "./SchemaBasedField.svelte";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import SchemaBasedArray from "./SchemaBasedArray.svelte";
|
||||
import SchemaBaseMultiType from "./SchemaBaseMultiType.svelte";
|
||||
import RegisteredTagInput from "./RegisteredTagInput.svelte";
|
||||
import SchemaBasedTranslationInput from "./SchemaBasedTranslationInput.svelte";
|
||||
|
||||
export let schema: ConfigMeta
|
||||
export let state: EditLayerState
|
||||
export let path: (string | number)[] = []
|
||||
|
||||
|
||||
console.log("Constructing", path,"with schema", schema)
|
||||
import type { ConfigMeta } from "./configMeta";
|
||||
import SchemaBasedField from "./SchemaBasedField.svelte";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import SchemaBasedArray from "./SchemaBasedArray.svelte";
|
||||
import SchemaBasedMultiType from "./SchemaBasedMultiType.svelte";
|
||||
import SchemaBasedTranslationInput from "./SchemaBasedTranslationInput.svelte";
|
||||
|
||||
export let schema: ConfigMeta;
|
||||
export let state: EditLayerState;
|
||||
export let path: (string | number)[] = [];
|
||||
|
||||
</script>
|
||||
{#if schema.hints.typehint === "tagrendering[]"}
|
||||
<!-- We cheat a bit here by matching this 'magical' type... -->
|
||||
<SchemaBasedArray {path} {state} {schema}/>
|
||||
<!-- We cheat a bit here by matching this 'magical' type... -->
|
||||
<SchemaBasedArray {path} {state} {schema} />
|
||||
{:else if schema.type === "array"}
|
||||
<SchemaBasedArray {path} {state} {schema}/>
|
||||
{:else if schema.hints.typehint === "tag"}
|
||||
<RegisteredTagInput {state} {path} {schema}/>
|
||||
<SchemaBasedArray {path} {state} {schema} />
|
||||
{:else if schema.type === "translation"}
|
||||
<SchemaBasedTranslationInput {path} {state} {schema}/>
|
||||
<SchemaBasedTranslationInput {path} {state} {schema} />
|
||||
{:else if schema.hints.types}
|
||||
<SchemaBaseMultiType {path} {state} {schema}/>
|
||||
<SchemaBasedMultiType {path} {state} {schema} />
|
||||
{:else}
|
||||
<SchemaBasedField {path} {state} {schema}/>
|
||||
<SchemaBasedField {path} {state} {schema} />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@
|
|||
const hasBooleanOption = (<JsonSchemaType[]>schema.type)?.findIndex(t => t["type"] === "boolean");
|
||||
const types = schema.hints.types.split(";");
|
||||
if (hasBooleanOption >= 0) {
|
||||
console.log(path.join("."), ": types are", types, ", boolean index is", hasBooleanOption);
|
||||
types.splice(hasBooleanOption);
|
||||
}
|
||||
|
||||
|
|
@ -2,118 +2,121 @@
|
|||
* Allows to create `and` and `or` expressions graphically
|
||||
*/
|
||||
|
||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {TagConfigJson} from "../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import type { TagConfigJson } from "../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import BasicTagInput from "./TagInput/BasicTagInput.svelte";
|
||||
import TagInput from "./TagInput/TagInput.svelte";
|
||||
import {TrashIcon} from "@babeard/svelte-heroicons/mini";
|
||||
import FullTagInput from "./TagInput/FullTagInput.svelte";
|
||||
import { TrashIcon } from "@babeard/svelte-heroicons/mini";
|
||||
|
||||
export let tag: UIEventSource<TagConfigJson>
|
||||
let mode: "and" | "or" = "and"
|
||||
export let tag: UIEventSource<TagConfigJson>;
|
||||
let mode: "and" | "or" = "and";
|
||||
|
||||
let basicTags: UIEventSource<UIEventSource<string>[]> = new UIEventSource([])
|
||||
let basicTags: UIEventSource<UIEventSource<string>[]> = new UIEventSource([]);
|
||||
|
||||
/**
|
||||
* Sub-expressions
|
||||
*/
|
||||
let expressions: UIEventSource<UIEventSource<TagConfigJson>[]> = new UIEventSource([])
|
||||
export let uploadableOnly: boolean
|
||||
export let overpassSupportNeeded: boolean
|
||||
let expressions: UIEventSource<UIEventSource<TagConfigJson>[]> = new UIEventSource([]);
|
||||
|
||||
export let uploadableOnly: boolean;
|
||||
export let overpassSupportNeeded: boolean;
|
||||
|
||||
export let silent: boolean;
|
||||
|
||||
function update(_) {
|
||||
let config: TagConfigJson = <any>{}
|
||||
let config: TagConfigJson = <any>{};
|
||||
if (!mode) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
const tags = []
|
||||
const tags = [];
|
||||
|
||||
const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat(expressions.data)
|
||||
const subpartSources = (<UIEventSource<string | TagConfigJson>[]>basicTags.data).concat(expressions.data);
|
||||
for (const src of subpartSources) {
|
||||
const t = src.data
|
||||
const t = src.data;
|
||||
if (!t) {
|
||||
// We indicate upstream that this value is invalid
|
||||
tag.setData(undefined)
|
||||
return
|
||||
tag.setData(undefined);
|
||||
return;
|
||||
}
|
||||
tags.push(t)
|
||||
tags.push(t);
|
||||
}
|
||||
if (tags.length === 1) {
|
||||
tag.setData(tags[0])
|
||||
tag.setData(tags[0]);
|
||||
} else {
|
||||
config[mode] = tags
|
||||
tag.setData(config)
|
||||
config[mode] = tags;
|
||||
tag.setData(config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function addBasicTag(value?: string) {
|
||||
const src = new UIEventSource(value)
|
||||
const src = new UIEventSource(value);
|
||||
basicTags.data.push(src);
|
||||
basicTags.ping()
|
||||
src.addCallbackAndRunD(_ => update(_))
|
||||
basicTags.ping();
|
||||
src.addCallbackAndRunD(_ => update(_));
|
||||
}
|
||||
|
||||
function removeTag(basicTag: UIEventSource<any>) {
|
||||
const index = basicTags.data.indexOf(basicTag)
|
||||
console.log("Removing", index, basicTag)
|
||||
const index = basicTags.data.indexOf(basicTag);
|
||||
console.log("Removing", index, basicTag);
|
||||
if (index >= 0) {
|
||||
basicTag.setData(undefined)
|
||||
basicTags.data.splice(index, 1)
|
||||
basicTags.ping()
|
||||
basicTag.setData(undefined);
|
||||
basicTags.data.splice(index, 1);
|
||||
basicTags.ping();
|
||||
}
|
||||
}
|
||||
|
||||
function removeExpression(expr: UIEventSource<any>) {
|
||||
const index = expressions.data.indexOf(expr)
|
||||
const index = expressions.data.indexOf(expr);
|
||||
if (index >= 0) {
|
||||
expr.setData(undefined)
|
||||
expressions.data.splice(index, 1)
|
||||
expressions.ping()
|
||||
expr.setData(undefined);
|
||||
expressions.data.splice(index, 1);
|
||||
expressions.ping();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function addExpression(expr?: TagConfigJson) {
|
||||
const src = new UIEventSource(expr)
|
||||
const src = new UIEventSource(expr);
|
||||
expressions.data.push(src);
|
||||
expressions.ping()
|
||||
src.addCallbackAndRunD(_ => update(_))
|
||||
expressions.ping();
|
||||
src.addCallbackAndRunD(_ => update(_));
|
||||
}
|
||||
|
||||
|
||||
$: update(mode)
|
||||
expressions.addCallback(_ => update(_))
|
||||
basicTags.addCallback(_ => update(_))
|
||||
$: update(mode);
|
||||
expressions.addCallback(_ => update(_));
|
||||
basicTags.addCallback(_ => update(_));
|
||||
|
||||
let initialTag: TagConfigJson = tag.data
|
||||
let initialTag: TagConfigJson = tag.data;
|
||||
|
||||
function initWith(initialTag: TagConfigJson) {
|
||||
if (typeof initialTag === "string") {
|
||||
addBasicTag(initialTag)
|
||||
return
|
||||
addBasicTag(initialTag);
|
||||
return;
|
||||
}
|
||||
mode = <"or" | "and">Object.keys(initialTag)[0]
|
||||
const subExprs = (<TagConfigJson[]>initialTag[mode])
|
||||
if (subExprs.length == 0) {
|
||||
return
|
||||
mode = <"or" | "and">Object.keys(initialTag)[0];
|
||||
const subExprs = (<TagConfigJson[]>initialTag[mode]);
|
||||
if (!subExprs || subExprs.length == 0) {
|
||||
return;
|
||||
}
|
||||
if (subExprs.length == 1) {
|
||||
initWith(subExprs[0])
|
||||
initWith(subExprs[0]);
|
||||
return;
|
||||
}
|
||||
for (const subExpr of subExprs) {
|
||||
if (typeof subExpr === "string") {
|
||||
addBasicTag(subExpr)
|
||||
addBasicTag(subExpr);
|
||||
} else {
|
||||
addExpression(subExpr)
|
||||
addExpression(subExpr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!initialTag) {
|
||||
addBasicTag()
|
||||
addBasicTag();
|
||||
} else {
|
||||
initWith(initialTag)
|
||||
initWith(initialTag);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -122,37 +125,44 @@ if (!initialTag) {
|
|||
|
||||
<div class="flex items-center">
|
||||
|
||||
<select bind:value={mode}>
|
||||
<option value="and">and</option>
|
||||
{#if !uploadableOnly}
|
||||
{#if !uploadableOnly}
|
||||
<select bind:value={mode}>
|
||||
<option value="and">and</option>
|
||||
<option value="or">or</option>
|
||||
{/if}
|
||||
</select>
|
||||
</select>
|
||||
{/if}
|
||||
|
||||
<div class="border-l-4 border-black flex flex-col ml-1 pl-1">
|
||||
{#each $basicTags as basicTag (basicTag)}
|
||||
<div class="flex">
|
||||
<BasicTagInput {overpassSupportNeeded} {uploadableOnly} tag={basicTag}/>
|
||||
<button class="border border-black rounded-full w-fit h-fit p-0" on:click={() => removeTag(basicTag)}>
|
||||
<TrashIcon class="w-4 h-4 p-1"/>
|
||||
</button>
|
||||
<BasicTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={basicTag} />
|
||||
{#if $basicTags.length + $expressions.length > 1}
|
||||
<button class="border border-black rounded-full w-fit h-fit p-0"
|
||||
on:click={() => removeTag(basicTag)}>
|
||||
<TrashIcon class="w-4 h-4 p-1" />
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#each $expressions as expression}
|
||||
<TagInput {overpassSupportNeeded} {uploadableOnly} tag={expression}>
|
||||
<button slot="delete" on:click={() => removeExpression(expression)}>
|
||||
<TrashIcon class="w-4 h-4 p-1"/>
|
||||
<FullTagInput {silent} {overpassSupportNeeded} {uploadableOnly} tag={expression}>
|
||||
<button class="small" slot="delete" on:click={() => removeExpression(expression)}>
|
||||
<TrashIcon class="w-3 h-3 p-0" />
|
||||
Delete subexpression
|
||||
</button>
|
||||
</TagInput>
|
||||
</FullTagInput>
|
||||
{/each}
|
||||
<div class="flex">
|
||||
<button class="w-fit" on:click={() => addBasicTag()}>
|
||||
<button class="w-fit small" on:click={() => addBasicTag()}>
|
||||
Add a tag
|
||||
</button>
|
||||
<button class="w-fit" on:click={() => addExpression()}>
|
||||
Add an expression
|
||||
</button>
|
||||
<slot name="delete"/>
|
||||
{#if !uploadableOnly}
|
||||
<!-- Do not allow to add an expression, as everything is 'and' anyway -->
|
||||
<button class="w-fit small" on:click={() => addExpression()}>
|
||||
Add an expression
|
||||
</button>
|
||||
{/if}
|
||||
<slot name="delete" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,58 +2,61 @@
|
|||
* A small component showing statistics from tagInfo.
|
||||
* Will show this in an 'alert' if very little (<250) tags are known
|
||||
*/
|
||||
import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource";
|
||||
import type {TagInfoStats} from "../../Logic/Web/TagInfo";
|
||||
import { TagUtils } from "../../Logic/Tags/TagUtils";
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import type { TagInfoStats } from "../../Logic/Web/TagInfo";
|
||||
import TagInfo from "../../Logic/Web/TagInfo";
|
||||
import {twMerge} from "tailwind-merge";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
|
||||
export let tag: UIEventSource<string>
|
||||
const tagStabilized = tag.stabilized(500)
|
||||
export let silent = false;
|
||||
export let tag: UIEventSource<string>;
|
||||
const tagStabilized = tag.stabilized(500);
|
||||
const tagInfoStats: Store<TagInfoStats> = tagStabilized.bind(tag => {
|
||||
if (!tag) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
|
||||
const t = TagUtils.Tag(tag)
|
||||
const k = t["key"]
|
||||
let v = t["value"]
|
||||
const t = TagUtils.Tag(tag);
|
||||
const k = t["key"];
|
||||
let v = t["value"];
|
||||
if (typeof v !== "string") {
|
||||
v = undefined
|
||||
v = undefined;
|
||||
}
|
||||
if (!k) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
return UIEventSource.FromPromise(TagInfo.global.getStats(k, v))
|
||||
return UIEventSource.FromPromise(TagInfo.global.getStats(k, v));
|
||||
} catch (e) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
});
|
||||
const tagInfoUrl: Store<string> = tagStabilized.mapD(tag => {
|
||||
try {
|
||||
|
||||
const t = TagUtils.Tag(tag)
|
||||
const k = t["key"]
|
||||
let v = t["value"]
|
||||
const t = TagUtils.Tag(tag);
|
||||
const k = t["key"];
|
||||
let v = t["value"];
|
||||
if (typeof v !== "string") {
|
||||
v = undefined
|
||||
v = undefined;
|
||||
}
|
||||
if (!k) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
return TagInfo.global.webUrl(k, v)
|
||||
return TagInfo.global.webUrl(k, v);
|
||||
} catch (e) {
|
||||
return undefined
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
const total = tagInfoStats.mapD(data => data.data.find(i => i.type === "all").count)
|
||||
});
|
||||
const total = tagInfoStats.mapD(data => data.data.find(i => i.type === "all").count);
|
||||
</script>
|
||||
|
||||
{#if $tagStabilized !== $tag}
|
||||
<Loading/>
|
||||
{:else if $tagInfoStats }
|
||||
{#if !silent}
|
||||
<Loading />
|
||||
{/if}
|
||||
{:else if $tagInfoStats && (!silent || $total < 250) }
|
||||
<a href={$tagInfoUrl} target="_blank" class={twMerge(($total < 250) ? "alert" : "thanks", "w-fit link-underline")}>
|
||||
{$total} features on OSM have this tag
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -6,11 +6,14 @@
|
|||
import Tr from "../../Base/Tr.svelte";
|
||||
import {TagUtils} from "../../../Logic/Tags/TagUtils";
|
||||
import TagInfoStats from "../TagInfoStats.svelte";
|
||||
import { Translation } from "../../i18n/Translation";
|
||||
|
||||
export let tag: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||
export let uploadableOnly: boolean
|
||||
export let overpassSupportNeeded: boolean
|
||||
|
||||
export let silent : boolean = false
|
||||
|
||||
let feedbackGlobal = tag.map(tag => {
|
||||
if (!tag) {
|
||||
return undefined
|
||||
|
|
@ -24,11 +27,11 @@
|
|||
|
||||
})
|
||||
|
||||
let feedbackKey = new UIEventSource<string>(undefined)
|
||||
let feedbackKey = new UIEventSource<Translation>(undefined)
|
||||
let keyValue = new UIEventSource<string>(undefined)
|
||||
|
||||
|
||||
let feedbackValue = new UIEventSource<string>(undefined)
|
||||
let feedbackValue = new UIEventSource<Translation>(undefined)
|
||||
/**
|
||||
* The value of the tag. The name is a bit confusing
|
||||
*/
|
||||
|
|
@ -79,7 +82,11 @@
|
|||
|
||||
function setTag(_) {
|
||||
const k = keyValue.data
|
||||
const v = valueValue.data
|
||||
const v = valueValue.data ?? ""
|
||||
if(k === undefined || k === ""){
|
||||
tag.setData(undefined)
|
||||
return
|
||||
}
|
||||
const t = k + mode + v
|
||||
try {
|
||||
TagUtils.Tag(t)
|
||||
|
|
@ -116,5 +123,5 @@
|
|||
{:else if $feedbackGlobal}
|
||||
<Tr cls="alert" t={$feedbackGlobal}/>
|
||||
{/if}
|
||||
<TagInfoStats {tag}/>
|
||||
<TagInfoStats {silent} {tag}/>
|
||||
</div>
|
||||
|
|
|
|||
19
src/UI/Studio/TagInput/FullTagInput.svelte
Normal file
19
src/UI/Studio/TagInput/FullTagInput.svelte
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<script lang="ts">/**
|
||||
* An element input a tag; has `and`, `or`, `regex`, ...
|
||||
*/
|
||||
import type { TagConfigJson } from "../../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import TagExpression from "../TagExpression.svelte";
|
||||
|
||||
|
||||
export let tag: UIEventSource<string | TagConfigJson>
|
||||
export let uploadableOnly: boolean
|
||||
export let overpassSupportNeeded: boolean
|
||||
export let silent: boolean
|
||||
</script>
|
||||
|
||||
<div class="m-2">
|
||||
<TagExpression {silent} {overpassSupportNeeded} {tag} {uploadableOnly}>
|
||||
<slot name="delete" slot="delete"/>
|
||||
</TagExpression>
|
||||
</div>
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<script lang="ts">/**
|
||||
* An element input a tag; has `and`, `or`, `regex`, ...
|
||||
*/
|
||||
import type {TagConfigJson} from "../../../Models/ThemeConfig/Json/TagConfigJson";
|
||||
import {UIEventSource} from "../../../Logic/UIEventSource";
|
||||
import TagExpression from "../TagExpression.svelte";
|
||||
|
||||
|
||||
export let tag: UIEventSource<TagConfigJson>
|
||||
export let uploadableOnly: boolean
|
||||
export let overpassSupportNeeded: boolean
|
||||
</script>
|
||||
|
||||
<div class="m-4">
|
||||
<TagExpression {overpassSupportNeeded} {tag} {uploadableOnly}>
|
||||
<slot name="delete" slot="delete"/>
|
||||
</TagExpression>
|
||||
</div>
|
||||
14
src/UI/Studio/TagRenderingFreeformInput.svelte
Normal file
14
src/UI/Studio/TagRenderingFreeformInput.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
|
||||
import SchemaBasedInput from "./SchemaBasedInput.svelte";
|
||||
import EditLayerState from "./EditLayerState";
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
|
||||
export let state: EditLayerState
|
||||
export let path : (number | string)[]
|
||||
|
||||
let schema : TagRenderingConfig
|
||||
|
||||
</script>
|
||||
|
||||
XYZ
|
||||
|
|
@ -12,6 +12,11 @@ import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
|||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import * as questions from "../../assets/generated/layers/questions.json";
|
||||
import MappingInput from "./MappingInput.svelte";
|
||||
import { TrashIcon } from "@rgossiaux/svelte-heroicons/outline";
|
||||
import questionableTagRenderingSchemaRaw from "../../assets/schemas/questionabletagrenderingconfigmeta.json";
|
||||
import SchemaBasedField from "./SchemaBasedField.svelte";
|
||||
import Region from "./Region.svelte";
|
||||
|
||||
export let state: EditLayerState;
|
||||
export let schema: ConfigMeta;
|
||||
|
|
@ -19,11 +24,11 @@ export let path: (string | number)[];
|
|||
|
||||
let value = state.getCurrentValueFor(path);
|
||||
|
||||
let mappings: MappingConfigJson[] = [];
|
||||
let mappingsBuiltin: MappingConfigJson[] = [];
|
||||
for (const tr of questions.tagRenderings) {
|
||||
let description = tr["description"] ?? tr["question"] ?? "No description available";
|
||||
description = description["en"] ?? description;
|
||||
mappings.push({
|
||||
mappingsBuiltin.push({
|
||||
if: "value=" + tr["id"],
|
||||
then: {
|
||||
"en": "Builtin <b>" + tr["id"] + "</b> <div class='subtle'>" + description + "</div>"
|
||||
|
|
@ -34,28 +39,77 @@ for (const tr of questions.tagRenderings) {
|
|||
|
||||
const configBuiltin = new TagRenderingConfig(<QuestionableTagRenderingConfigJson>{
|
||||
question: "Which builtin element should be shown?",
|
||||
mappings
|
||||
mappings: mappingsBuiltin
|
||||
});
|
||||
|
||||
|
||||
const configOverride = <QuestionableTagRenderingConfigJson>{
|
||||
render: "This is a builtin question which changes some properties. Editing those is not possible within MapComplete Studio"
|
||||
};
|
||||
|
||||
const tags = new UIEventSource({ value });
|
||||
|
||||
tags.addCallbackAndRunD(tgs => {
|
||||
state.setValueAt(path, tgs["value"]);
|
||||
});
|
||||
|
||||
let mappings: UIEventSource<MappingConfigJson[]> = state.getStoreFor([...path, "mappings"]);
|
||||
|
||||
const topLevelItems: Record<string, ConfigMeta> = {};
|
||||
for (const item of questionableTagRenderingSchemaRaw) {
|
||||
if (item.path.length === 1) {
|
||||
topLevelItems[item.path[0]] = <ConfigMeta>item;
|
||||
}
|
||||
}
|
||||
|
||||
function initMappings() {
|
||||
if (mappings.data === undefined) {
|
||||
mappings.setData([]);
|
||||
}
|
||||
}
|
||||
|
||||
const freeformSchema = <ConfigMeta[]> questionableTagRenderingSchemaRaw.filter(schema => schema.path.length >= 1 && schema.path[0] === "freeform");
|
||||
console.log("FreeformSchema:", freeformSchema)
|
||||
</script>
|
||||
|
||||
{#if typeof value === "string"}
|
||||
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state}
|
||||
{tags} />
|
||||
|
||||
{:else}
|
||||
<div>
|
||||
TR{JSON.stringify(state.getCurrentValueFor(path))}
|
||||
<div class="flex low-interaction">
|
||||
<TagRenderingEditable config={configBuiltin} selectedElement={undefined} showQuestionIfUnknown={true} {state}
|
||||
{tags} />
|
||||
<slot name="upper-right" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col w-full p-1 gap-y-1">
|
||||
<div class="flex justify-end">
|
||||
<slot name="upper-right" />
|
||||
</div>
|
||||
|
||||
<SchemaBasedField {state} path={[...path,"question"]} schema={topLevelItems["question"]}></SchemaBasedField>
|
||||
<SchemaBasedField {state} path={[...path,"questionHint"]} schema={topLevelItems["questionHint"]}></SchemaBasedField>
|
||||
|
||||
{#each ($mappings ?? []) as mapping, i (mapping)}
|
||||
<div class="flex interactive w-full">
|
||||
<MappingInput {mapping} {state} path={path.concat(["mappings", i])}>
|
||||
<button slot="delete" class="rounded-full no-image-background" on:click={() => {
|
||||
initMappings();
|
||||
mappings.data.splice(i, 1)
|
||||
mappings.ping()
|
||||
}}>
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</MappingInput>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<button class="small primary"
|
||||
on:click={() =>{ initMappings(); mappings.data.push({if: undefined, then: {}}); mappings.ping()} }>
|
||||
Add a mapping
|
||||
</button>
|
||||
|
||||
<Region {state} {path} configs={freeformSchema}/>
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- {JSON.stringify(state.getCurrentValueFor(path))} <!-->
|
||||
</div>
|
||||
<!--
|
||||
<Region configs={freeformSchema} {state} path={[...path, "freeform"]} /> -->
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,11 @@
|
|||
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
|
||||
Main action (disabled)
|
||||
</button>
|
||||
|
||||
<button class="small">
|
||||
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
|
||||
Small button
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button>
|
||||
|
|
@ -91,6 +96,10 @@
|
|||
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
|
||||
Main action (disabled)
|
||||
</button>
|
||||
<button class="small">
|
||||
<ToSvelte construct={Svg.community_svg().SetClass("w-6 h-6")} />
|
||||
Small button
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button>
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ export class Translation extends BaseUIElement {
|
|||
if (count === 0) {
|
||||
console.error(
|
||||
"Constructing a translation, but the object containing translations is empty " +
|
||||
context
|
||||
(context ?? "No context given")
|
||||
)
|
||||
throw `Constructing a translation, but the object containing translations is empty (${context})`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue