forked from MapComplete/MapComplete
Add question box as special rendering
This commit is contained in:
parent
15664df63f
commit
d47fd7e746
42 changed files with 956 additions and 311 deletions
|
@ -13,7 +13,7 @@
|
|||
// Text for the current language
|
||||
let txt: string | undefined;
|
||||
|
||||
onDestroy(Locale.language.addCallbackAndRunD(l => {
|
||||
$: onDestroy(Locale.language.addCallbackAndRunD(l => {
|
||||
const translation = t?.textFor(l)
|
||||
if(translation === undefined){
|
||||
return
|
||||
|
|
|
@ -3,52 +3,32 @@
|
|||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import TagRenderingAnswer from "../Popup/TagRenderingAnswer.svelte";
|
||||
import TagRenderingQuestion from "../Popup/TagRenderingQuestion.svelte";
|
||||
import TagRenderingAnswer from "../Popup/TagRendering/TagRenderingAnswer.svelte";
|
||||
import TagRenderingEditable from "../Popup/TagRendering/TagRenderingEditable.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let selectedElement: Feature;
|
||||
export let layer: LayerConfig;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
|
||||
let _tags: Record<string, string>;
|
||||
onDestroy(tags.addCallbackAndRun(tags => {
|
||||
_tags = tags;
|
||||
}));
|
||||
export let state: SpecialVisualizationState;
|
||||
|
||||
/**
|
||||
* const title = new TagRenderingAnswer(
|
||||
* tags,
|
||||
* layerConfig.title ?? new TagRenderingConfig("POI"),
|
||||
* state
|
||||
* ).SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2 text-2xl")
|
||||
* const titleIcons = new Combine(
|
||||
* layerConfig.titleIcons.map((icon) => {
|
||||
* return new TagRenderingAnswer(
|
||||
* tags,
|
||||
* icon,
|
||||
* state,
|
||||
* "block h-8 max-h-8 align-baseline box-content sm:p-0.5 titleicon"
|
||||
* )
|
||||
* })
|
||||
* ).SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2")
|
||||
*
|
||||
* return new Combine([
|
||||
* new Combine([title, titleIcons]).SetClass(
|
||||
* "flex flex-col sm:flex-row flex-grow justify-between"
|
||||
* ),
|
||||
* ])
|
||||
*/
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="flex flex-col sm:flex-row flex-grow justify-between">
|
||||
<!-- Title element-->
|
||||
<h3>
|
||||
<TagRenderingAnswer config={layer.title} {selectedElement} {tags}></TagRenderingAnswer>
|
||||
<TagRenderingAnswer config={layer.title} {selectedElement} {state} {tags} {layer}></TagRenderingAnswer>
|
||||
</h3>
|
||||
|
||||
<div class="flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2">
|
||||
{#each layer.titleIcons as titleIconConfig (titleIconConfig.id)}
|
||||
<div class="w-8 h-8">
|
||||
<TagRenderingAnswer config={titleIconConfig} {tags} {selectedElement}></TagRenderingAnswer>
|
||||
<TagRenderingAnswer config={titleIconConfig} {tags} {selectedElement} {state} {layer}></TagRenderingAnswer>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -58,10 +38,10 @@
|
|||
|
||||
<div class="flex flex-col">
|
||||
{#each layer.tagRenderings as config (config.id)}
|
||||
{#if config.IsKnown($tags)}
|
||||
<TagRenderingAnswer {tags} {config} {state}></TagRenderingAnswer>
|
||||
{:else}
|
||||
<TagRenderingQuestion {config} {tags} {state}></TagRenderingQuestion>
|
||||
{#if config.condition === undefined || config.condition.matchesProperties(_tags)}
|
||||
{#if config.IsKnown(_tags)}
|
||||
<TagRenderingEditable {tags} {config} {state} {selectedElement} {layer}></TagRenderingEditable>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -1,41 +1,53 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import type { ValidatorType } from "./Validators";
|
||||
import Validators from "./Validators";
|
||||
import { ExclamationIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let value: UIEventSource<string>;
|
||||
// Internal state, only copied to 'value' so that no invalid values leak outside
|
||||
let _value = new UIEventSource(value.data ?? "")
|
||||
let _value = new UIEventSource(value.data ?? "");
|
||||
export let type: ValidatorType;
|
||||
let validator = Validators.get(type);
|
||||
export let feedback: UIEventSource<Translation> | undefined = undefined
|
||||
export let feedback: UIEventSource<Translation> | undefined = undefined;
|
||||
_value.addCallbackAndRun(v => {
|
||||
if(validator.isValid(v)){
|
||||
feedback?.setData(undefined)
|
||||
value.setData(v)
|
||||
return
|
||||
if (validator.isValid(v)) {
|
||||
feedback?.setData(undefined);
|
||||
value.setData(v);
|
||||
return;
|
||||
}
|
||||
value.setData(undefined)
|
||||
value.setData(undefined);
|
||||
feedback?.setData(validator.getFeedback(v));
|
||||
})
|
||||
});
|
||||
|
||||
if (validator === undefined) {
|
||||
throw "Not a valid type for a validator:" + type;
|
||||
}
|
||||
|
||||
const isValid = _value.map(v => validator.isValid(v));
|
||||
|
||||
|
||||
let htmlElem: HTMLInputElement;
|
||||
|
||||
let dispatch = createEventDispatcher<{ selected }>();
|
||||
$: {
|
||||
console.log(htmlElem)
|
||||
if (htmlElem !== undefined) {
|
||||
htmlElem.onfocus = () => {
|
||||
console.log("Dispatching selected event")
|
||||
return dispatch("selected");
|
||||
};
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if validator.textArea}
|
||||
<textarea bind:value={$_value} inputmode={validator.inputmode ?? "text"}></textarea>
|
||||
{:else }
|
||||
<div class="flex">
|
||||
<input bind:value={$_value} inputmode={validator.inputmode ?? "text"}>
|
||||
<input bind:this={htmlElem} bind:value={$_value} inputmode={validator.inputmode ?? "text"}>
|
||||
{#if !$isValid}
|
||||
<ExclamationIcon class="h-6 w-6 -ml-6"></ExclamationIcon>
|
||||
{/if}
|
||||
|
|
51
UI/Popup/QuestionViz.ts
Normal file
51
UI/Popup/QuestionViz.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { SpecialVisualization, SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import { Feature } from "geojson"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
import SvelteUIElement from "../Base/SvelteUIElement"
|
||||
import Questionbox from "./TagRendering/Questionbox.svelte"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
|
||||
/**
|
||||
* Thin wrapper around QuestionBox.svelte to include it into the special Visualisations
|
||||
*/
|
||||
export default class QuestionViz implements SpecialVisualization {
|
||||
funcName = "questions"
|
||||
docs =
|
||||
"The special element which shows the questions which are unkown. Added by default if not yet there"
|
||||
args = [
|
||||
{
|
||||
name: "labels",
|
||||
doc: "One or more ';'-separated labels. If these are given, only questions with these labels will be given. Use `unlabeled` for all questions that don't have an explicit label. If none given, all questions will be shown",
|
||||
},
|
||||
{
|
||||
name: "blacklisted-labels",
|
||||
doc: "One or more ';'-separated labels of questions which should _not_ be included",
|
||||
},
|
||||
]
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tags: UIEventSource<Record<string, string>>,
|
||||
args: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement {
|
||||
const labels = args[0]
|
||||
?.split(";")
|
||||
?.map((s) => s.trim())
|
||||
?.filter((s) => s !== "")
|
||||
const blacklist = args[1]
|
||||
?.split(";")
|
||||
?.map((s) => s.trim())
|
||||
?.filter((s) => s !== "")
|
||||
return new SvelteUIElement(Questionbox, {
|
||||
layer,
|
||||
tags,
|
||||
selectedElement: feature,
|
||||
state,
|
||||
onlyForLabels: labels,
|
||||
notForLabels: blacklist,
|
||||
})
|
||||
}
|
||||
}
|
26
UI/Popup/TagRendering/FreeformInput.svelte
Normal file
26
UI/Popup/TagRendering/FreeformInput.svelte
Normal file
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import { Translation } from "../../i18n/Translation";
|
||||
import ValidatedInput from "../../InputElement/ValidatedInput.svelte";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import Inline from "./Inline.svelte";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let value: UIEventSource<string>;
|
||||
export let config: TagRenderingConfig;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
|
||||
let feedback: UIEventSource<Translation> = new UIEventSource<Translation>(undefined);
|
||||
|
||||
let dispatch = createEventDispatcher<{ "selected" }>();
|
||||
</script>
|
||||
<Inline key={config.freeform.key} {tags} template={config.render}>
|
||||
<ValidatedInput {feedback} type={config.freeform.type}
|
||||
{value} on:selected={() => dispatch("selected")}></ValidatedInput>
|
||||
</Inline>
|
||||
{#if $feedback !== undefined}
|
||||
<div class="alert">
|
||||
<Tr t={$feedback} />
|
||||
</div>
|
||||
{/if}
|
26
UI/Popup/TagRendering/Inline.svelte
Normal file
26
UI/Popup/TagRendering/Inline.svelte
Normal file
|
@ -0,0 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { Utils } from "../../../Utils.js";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
import { Translation } from "../../i18n/Translation";
|
||||
import Locale from "../../i18n/Locale";
|
||||
|
||||
export let template: Translation;
|
||||
let _template: string
|
||||
onDestroy(Locale.language.addCallbackAndRunD(l => {
|
||||
_template = template.textFor(l)
|
||||
}))
|
||||
export let key: string;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
let _tags = tags.data;
|
||||
onDestroy(tags.addCallbackAndRunD(tags => {
|
||||
_tags = tags;
|
||||
}));
|
||||
let [before, after] = _template.split("{" + key + "}");
|
||||
</script>
|
||||
|
||||
<span>
|
||||
{Utils.SubstituteKeys(before, _tags)}
|
||||
<slot />
|
||||
{Utils.SubstituteKeys(after, _tags)}
|
||||
</span>
|
108
UI/Popup/TagRendering/Questionbox.svelte
Normal file
108
UI/Popup/TagRendering/Questionbox.svelte
Normal file
|
@ -0,0 +1,108 @@
|
|||
<script lang="ts">
|
||||
|
||||
/**
|
||||
* Shows all questions for which the answers are unknown.
|
||||
* The questions can either be shown all at once or one at a time (in which case they can be skipped)
|
||||
*/
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { Feature } from "geojson";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
import If from "../../Base/If.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
|
||||
export let layer: LayerConfig;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let selectedElement: Feature;
|
||||
export let state: SpecialVisualizationState;
|
||||
|
||||
/**
|
||||
* If set, only questions for these labels will be shown
|
||||
*/
|
||||
export let onlyForLabels: string[] | undefined = undefined;
|
||||
const _onlyForLabels = new Set(onlyForLabels);
|
||||
/**
|
||||
* If set, only questions _not_ having these labels will be shown
|
||||
*/
|
||||
export let notForLabels: string[] | undefined = undefined;
|
||||
const _notForLabels = new Set(notForLabels);
|
||||
|
||||
function allowed(labels: string[]) {
|
||||
if (onlyForLabels?.length > 0 && !labels.some(l => _onlyForLabels.has(l))) {
|
||||
return false;
|
||||
}
|
||||
if (notForLabels?.length > 0 && labels.some(l => _notForLabels.has(l))) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log("Got layer", layer, onlyForLabels, notForLabels);
|
||||
|
||||
const baseQuestions = (layer.tagRenderings ?? [])?.filter(tr => allowed(tr.labels) && tr.question !== undefined);
|
||||
console.log("BaseQuestions are", baseQuestions);
|
||||
let skippedQuestions = new UIEventSource<Set<string>>(new Set<string>());
|
||||
let answered : number = 0
|
||||
let questionsToAsk = tags.map(tags => {
|
||||
const questionsToAsk: TagRenderingConfig[] = [];
|
||||
for (const baseQuestion of baseQuestions) {
|
||||
if (skippedQuestions.data.has(baseQuestion.id) > 0) {
|
||||
continue;
|
||||
}
|
||||
if (baseQuestion.condition !== undefined && !baseQuestion.condition.matchesProperties(tags)) {
|
||||
continue;
|
||||
}
|
||||
questionsToAsk.push(baseQuestion);
|
||||
}
|
||||
return questionsToAsk;
|
||||
|
||||
}, [skippedQuestions]);
|
||||
let _questionsToAsk: TagRenderingConfig[];
|
||||
let _firstQuestion: TagRenderingConfig
|
||||
onDestroy(questionsToAsk.subscribe(qta => {
|
||||
_questionsToAsk = qta;
|
||||
_firstQuestion = qta[0]
|
||||
}));
|
||||
|
||||
function skip(question: TagRenderingConfig, didAnswer: boolean = false) {
|
||||
skippedQuestions.data.add(question.id);
|
||||
skippedQuestions.ping();
|
||||
if(didAnswer ){
|
||||
answered ++
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if _questionsToAsk.length === 0}
|
||||
All done! You answered {answered} questions and skipped {$skippedQuestions.size} questions.
|
||||
{#if $skippedQuestions.size > 0 }
|
||||
<button on:click={() => skippedQuestions.setData(new Set())}>Re-activate skipped questions</button>
|
||||
{/if}
|
||||
{:else }
|
||||
<div>
|
||||
<If condition={state.userRelatedState.showAllQuestionsAtOnce}>
|
||||
<div>
|
||||
{#each _questionsToAsk as question (question.id)}
|
||||
<TagRenderingQuestion config={question} {tags} {selectedElement} {state} {layer}></TagRenderingQuestion>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div slot="else">
|
||||
<TagRenderingQuestion
|
||||
config={_firstQuestion} {layer} {selectedElement} {state} {tags}
|
||||
on:saved={() => {skip(_firstQuestion, true)}}>
|
||||
<button on:click={() => {skip(_firstQuestion)} }
|
||||
slot="cancel">
|
||||
<Tr t={Translations.t.general.skip}></Tr>
|
||||
</button>
|
||||
</TagRenderingQuestion>
|
||||
|
||||
</div>
|
||||
|
||||
</If>
|
||||
</div>
|
||||
{/if}
|
|
@ -1,14 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import SpecialVisualizations from "../SpecialVisualizations";
|
||||
import { Translation } from "../../i18n/Translation";
|
||||
import SpecialVisualizations from "../../SpecialVisualizations";
|
||||
import { onDestroy } from "svelte";
|
||||
import Locale from "../i18n/Locale";
|
||||
import type { RenderingSpecification, SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { Utils } from "../../Utils.js";
|
||||
import Locale from "../../i18n/Locale";
|
||||
import type { RenderingSpecification, SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import { Utils } from "../../../Utils.js";
|
||||
import type { Feature } from "geojson";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource.js";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import FromHtml from "../Base/FromHtml.svelte";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource.js";
|
||||
import ToSvelte from "../../Base/ToSvelte.svelte";
|
||||
import FromHtml from "../../Base/FromHtml.svelte";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
/**
|
||||
* The 'specialTranslation' renders a `Translation`-object, but interprets the special values as well
|
||||
|
@ -17,18 +18,18 @@
|
|||
export let state: SpecialVisualizationState;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let feature: Feature;
|
||||
export let layer: LayerConfig
|
||||
let txt: string;
|
||||
onDestroy(Locale.language.addCallbackAndRunD(l => {
|
||||
txt = t.textFor(l);
|
||||
}));
|
||||
let specs: RenderingSpecification[];
|
||||
specs = SpecialVisualizations.constructSpecification(txt);
|
||||
let specs: RenderingSpecification[] = SpecialVisualizations.constructSpecification(txt);
|
||||
</script>
|
||||
|
||||
{#each specs as specpart}
|
||||
{#if typeof specpart === "string"}
|
||||
<FromHtml src= {Utils.SubstituteKeys(specpart, $tags)}></FromHtml>
|
||||
{:else if $tags !== undefined }
|
||||
<ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature)}></ToSvelte>
|
||||
<ToSvelte construct={specpart.func.constr(state, tags, specpart.args, feature, layer)}></ToSvelte>
|
||||
{/if}
|
||||
{/each}
|
38
UI/Popup/TagRendering/TagRenderingAnswer.svelte
Normal file
38
UI/Popup/TagRendering/TagRenderingAnswer.svelte
Normal file
|
@ -0,0 +1,38 @@
|
|||
<script lang="ts">
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import { Utils } from "../../../Utils";
|
||||
import { Translation } from "../../i18n/Translation";
|
||||
import TagRenderingMapping from "./TagRenderingMapping.svelte";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import type { Feature } from "geojson";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
export let tags: UIEventSource<Record<string, string> | undefined>;
|
||||
let _tags: Record<string, string>;
|
||||
onDestroy(tags.addCallbackAndRun(tags => {
|
||||
_tags = tags;
|
||||
}));
|
||||
export let state: SpecialVisualizationState;
|
||||
export let selectedElement: Feature;
|
||||
export let config: TagRenderingConfig;
|
||||
export let layer: LayerConfig
|
||||
let trs: { then: Translation; icon?: string; iconClass?: string }[];
|
||||
$: trs = Utils.NoNull(config?.GetRenderValues(_tags));
|
||||
</script>
|
||||
|
||||
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(_tags))}
|
||||
{#if trs.length === 1}
|
||||
<TagRenderingMapping mapping={trs[0]} {tags} {state} {selectedElement} {layer}></TagRenderingMapping>
|
||||
{/if}
|
||||
{#if trs.length > 1}
|
||||
<ul>
|
||||
{#each trs as mapping}
|
||||
<li>
|
||||
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer}></TagRenderingMapping>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/if}
|
46
UI/Popup/TagRendering/TagRenderingEditable.svelte
Normal file
46
UI/Popup/TagRendering/TagRenderingEditable.svelte
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script lang="ts">
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { Feature } from "geojson";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import TagRenderingAnswer from "./TagRenderingAnswer.svelte";
|
||||
import { PencilAltIcon } from "@rgossiaux/svelte-heroicons/solid";
|
||||
import TagRenderingQuestion from "./TagRenderingQuestion.svelte";
|
||||
import { onDestroy } from "svelte";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
export let config: TagRenderingConfig;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let selectedElement: Feature;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let layer: LayerConfig
|
||||
|
||||
export let showQuestionIfUnknown : boolean= false
|
||||
let editMode = false
|
||||
onDestroy(tags.addCallbackAndRunD(tags => {
|
||||
editMode = showQuestionIfUnknown && !config.IsKnown(tags)
|
||||
|
||||
}))
|
||||
|
||||
</script>
|
||||
|
||||
{#if config.question}
|
||||
{#if editMode}
|
||||
<TagRenderingQuestion {config} {tags} {selectedElement} {state} {layer} >
|
||||
<button slot="cancel" on:click={() => {editMode = false}}>
|
||||
<Tr t={Translations.t.general.cancel}/>
|
||||
</button>
|
||||
</TagRenderingQuestion>
|
||||
{:else}
|
||||
<div class="flex justify-between">
|
||||
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
|
||||
<button on:click={() => {editMode = true}} class="w-6 h-6 rounded-full subtle-background p-1">
|
||||
<PencilAltIcon></PencilAltIcon>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else }
|
||||
<TagRenderingAnswer {config} {tags} {selectedElement} {state} {layer} />
|
||||
{/if}
|
|
@ -1,13 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import { Translation } from "../../i18n/Translation";
|
||||
import SpecialTranslation from "./SpecialTranslation.svelte";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import type { Feature } from "geojson";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
export let selectedElement: Feature
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let state: SpecialVisualizationState
|
||||
export let layer: LayerConfig
|
||||
export let mapping: {
|
||||
then: Translation; icon?: string; iconClass?: | "small"
|
||||
| "medium"
|
||||
|
@ -22,11 +24,11 @@
|
|||
</script>
|
||||
|
||||
{#if mapping.icon !== undefined}
|
||||
<div class="flex">
|
||||
<div class="inline-flex">
|
||||
<img class={iconclass+" mr-1"} src={mapping.icon}>
|
||||
<SpecialTranslation t={mapping.then} {tags} {state} feature={selectedElement}></SpecialTranslation>
|
||||
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
|
||||
</div>
|
||||
{:else if mapping.then !== undefined}
|
||||
<SpecialTranslation t={mapping.then} {tags} {state} feature={selectedElement}></SpecialTranslation>
|
||||
<SpecialTranslation t={mapping.then} {tags} {state} {layer} feature={selectedElement}></SpecialTranslation>
|
||||
{/if}
|
||||
|
147
UI/Popup/TagRendering/TagRenderingQuestion.svelte
Normal file
147
UI/Popup/TagRendering/TagRenderingQuestion.svelte
Normal file
|
@ -0,0 +1,147 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../../Logic/UIEventSource";
|
||||
import type { SpecialVisualizationState } from "../../SpecialVisualization";
|
||||
import Tr from "../../Base/Tr.svelte";
|
||||
import If from "../../Base/If.svelte";
|
||||
import TagRenderingMapping from "./TagRenderingMapping.svelte";
|
||||
import type { Feature } from "geojson";
|
||||
import type { Mapping } from "../../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import TagRenderingConfig from "../../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import { TagsFilter } from "../../../Logic/Tags/TagsFilter";
|
||||
import FreeformInput from "./FreeformInput.svelte";
|
||||
import Translations from "../../i18n/Translations.js";
|
||||
import FromHtml from "../../Base/FromHtml.svelte";
|
||||
import ChangeTagAction from "../../../Logic/Osm/Actions/ChangeTagAction";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig";
|
||||
|
||||
export let config: TagRenderingConfig;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let selectedElement: Feature;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let layer: LayerConfig;
|
||||
|
||||
// Will be bound if a freeform is available
|
||||
let freeformInput = new UIEventSource<string>(undefined);
|
||||
let selectedMapping: number = 0;
|
||||
let checkedMappings: boolean[];
|
||||
if (config.mappings?.length > 0) {
|
||||
checkedMappings = [...config.mappings.map(_ => false), false /*One element extra in case a freeform value is added*/];
|
||||
}
|
||||
let selectedTags: TagsFilter = undefined;
|
||||
$:selectedTags = config?.constructChangeSpecification($freeformInput, selectedMapping, checkedMappings);
|
||||
|
||||
function mappingIsHidden(mapping: Mapping): boolean {
|
||||
if (mapping.hideInAnswer === undefined || mapping.hideInAnswer === false) {
|
||||
return false;
|
||||
}
|
||||
if (mapping.hideInAnswer === true) {
|
||||
return true;
|
||||
}
|
||||
return (<TagsFilter>mapping.hideInAnswer).matchesProperties(tags.data);
|
||||
}
|
||||
|
||||
let dispatch = createEventDispatcher<{
|
||||
"saved": {
|
||||
config: TagRenderingConfig,
|
||||
applied: TagsFilter
|
||||
}
|
||||
}>();
|
||||
|
||||
function onSave() {
|
||||
dispatch("saved", { config, applied: selectedTags });
|
||||
const change = new ChangeTagAction(
|
||||
tags.data.id,
|
||||
selectedTags,
|
||||
tags.data,
|
||||
{
|
||||
theme: state.layout.id,
|
||||
changeType: "answer"
|
||||
}
|
||||
);
|
||||
change.CreateChangeDescriptions().then(changes =>
|
||||
state.changes.applyChanges(changes)
|
||||
).catch(console.error);
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{#if config.question !== undefined}
|
||||
<div class="border border-black subtle-background flex flex-col">
|
||||
<If condition={state.featureSwitchIsTesting}>
|
||||
<div class="flex justify-between">
|
||||
<Tr t={config.question}></Tr>
|
||||
<span class="alert">{config.id}</span>
|
||||
</div>
|
||||
<Tr slot="else" t={config.question}></Tr>
|
||||
</If>
|
||||
|
||||
{#if config.questionhint}
|
||||
<div class="subtle">
|
||||
<Tr t={config.questionHint}></Tr>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if config.freeform?.key && !(config.mappings?.length > 0)}
|
||||
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
|
||||
<FreeformInput {config} {tags} value={freeformInput} />
|
||||
{/if}
|
||||
|
||||
{#if config.mappings !== undefined && !config.multiAnswer}
|
||||
<!-- Simple radiobuttons as mapping -->
|
||||
<div class="flex flex-col">
|
||||
{#each config.mappings as mapping, i (mapping.then)}
|
||||
{#if !mappingIsHidden(mapping) }
|
||||
<label>
|
||||
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id} value={i}>
|
||||
<TagRenderingMapping {mapping} {tags} {state} {selectedElement} {layer}></TagRenderingMapping>
|
||||
</label>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if config.freeform?.key}
|
||||
<label>
|
||||
<input type="radio" bind:group={selectedMapping} name={"mappings-radio-"+config.id}
|
||||
value={config.mappings.length}>
|
||||
<FreeformInput {config} {tags} value={freeformInput}
|
||||
on:selected={() => selectedMapping = config.mappings.length } />
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
{#if config.mappings !== undefined && config.multiAnswer}
|
||||
<!-- Multiple answers can be chosen: checkboxes -->
|
||||
<div class="flex flex-col">
|
||||
{#each config.mappings as mapping, i (mapping.then)}
|
||||
{#if !mappingIsHidden(mapping) }
|
||||
<label>
|
||||
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+i} bind:checked={checkedMappings[i]}>
|
||||
<TagRenderingMapping {mapping} {tags} {state} {selectedElement}></TagRenderingMapping>
|
||||
</label>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if config.freeform?.key}
|
||||
<label>
|
||||
<input type="checkbox" name={"mappings-checkbox-"+config.id+"-"+config.mappings.length}
|
||||
bind:checked={checkedMappings[config.mappings.length]}>
|
||||
<FreeformInput {config} {tags} value={freeformInput}
|
||||
on:selected={() => checkedMappings[config.mappings.length] = true} />
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<FromHtml src={selectedTags?.asHumanString(true, true, {})} />
|
||||
|
||||
<div>
|
||||
<!-- TagRenderingQuestion-buttons -->
|
||||
<slot name="cancel"></slot>
|
||||
<button on:click={onSave}>
|
||||
<Tr t={Translations.t.general.save}></Tr>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{/if}
|
|
@ -1,34 +0,0 @@
|
|||
<script lang="ts">
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
import { Utils } from "../../Utils";
|
||||
import { Translation } from "../i18n/Translation";
|
||||
import TagRenderingMapping from "./TagRenderingMapping.svelte";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import type { Feature } from "geojson";
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import { onDestroy } from "svelte";
|
||||
|
||||
export let tags: UIEventSource<Record<string, string> | undefined>;
|
||||
let _tags : Record<string, string>
|
||||
onDestroy(tags.addCallbackAndRun(tags => {
|
||||
_tags = tags
|
||||
}))
|
||||
export let state: SpecialVisualizationState
|
||||
export let selectedElement: Feature
|
||||
export let config: TagRenderingConfig;
|
||||
let trs: { then: Translation; icon?: string; iconClass?: string }[];
|
||||
$: trs = Utils.NoNull(config?.GetRenderValues(_tags));
|
||||
</script>
|
||||
|
||||
{#if config !== undefined && (config?.condition === undefined || config.condition.matchesProperties(tags))}
|
||||
<div>
|
||||
{#if trs.length === 1}
|
||||
<TagRenderingMapping mapping={trs[0]} {tags} {state} feature={selectedElement}></TagRenderingMapping>
|
||||
{/if}
|
||||
{#if trs.length > 1}
|
||||
{#each trs as mapping}
|
||||
<TagRenderingMapping mapping={trs} {tags} {state} feature=""{selectedElement}></TagRenderingMapping>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
|
@ -1,53 +0,0 @@
|
|||
<script lang="ts">
|
||||
import { UIEventSource } from "../../Logic/UIEventSource";
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import If from "../Base/If.svelte";
|
||||
import ValidatedInput from "../InputElement/ValidatedInput.svelte";
|
||||
import TagRenderingMapping from "./TagRenderingMapping.svelte";
|
||||
import type { Feature } from "geojson";
|
||||
import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig";
|
||||
|
||||
export let config: TagRenderingConfig;
|
||||
export let tags: UIEventSource<Record<string, string>>;
|
||||
export let selectedElement: Feature;
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
state.featureSwitchIsTesting;
|
||||
|
||||
let freeformInput = new UIEventSource<string>(undefined);
|
||||
</script>
|
||||
|
||||
{#if config.question !== undefined}
|
||||
<div class="border border-black subtle-background">
|
||||
<If condition={state.featureSwitchIsTesting}>
|
||||
<div class="flex justify-between">
|
||||
<Tr t={config.question}></Tr>
|
||||
{config.id}
|
||||
</div>
|
||||
<Tr slot="else" t={config.question}></Tr>
|
||||
</If>
|
||||
|
||||
{#if config.questionhint}
|
||||
<div class="subtle">
|
||||
<Tr t={config.question}></Tr>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if config.freeform?.key && !(config.mappings?.length > 0)}
|
||||
<!-- There are no options to choose from, simply show the input element: fill out the text field -->
|
||||
<ValidatedInput type={config.freeform.type} value={freeformInput}></ValidatedInput>
|
||||
{/if}
|
||||
|
||||
{#if config.mappings !== undefined}
|
||||
<div class="flex flex-col">
|
||||
{#each config.mappings as mapping}
|
||||
{#if mapping.hideInAnswer === true || !(mapping.hideInAnswer) || (console.log(tags) || true) || !(mapping.hideInAnswer?.matchesProperties($tags)) }
|
||||
<TagRenderingMapping {mapping} {tags} {state} {selectedElement}></TagRenderingMapping>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</div>
|
||||
{/if}
|
|
@ -11,6 +11,7 @@ import { Feature, Geometry } from "geojson"
|
|||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
|
||||
/**
|
||||
* The state needed to render a special Visualisation.
|
||||
|
@ -47,7 +48,10 @@ export interface SpecialVisualizationState {
|
|||
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
||||
|
||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||
readonly userRelatedState: { readonly mangroveIdentity: MangroveIdentity }
|
||||
readonly userRelatedState: {
|
||||
readonly mangroveIdentity: MangroveIdentity
|
||||
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
||||
}
|
||||
}
|
||||
|
||||
export interface SpecialVisualization {
|
||||
|
@ -73,7 +77,8 @@ export interface SpecialVisualization {
|
|||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ import FeatureReviews from "../Logic/Web/MangroveReviews"
|
|||
import Maproulette from "../Logic/Maproulette"
|
||||
import SvelteUIElement from "./Base/SvelteUIElement"
|
||||
import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource"
|
||||
import QuestionViz from "./Popup/QuestionViz"
|
||||
|
||||
export default class SpecialVisualizations {
|
||||
public static specialVisualizations: SpecialVisualization[] = SpecialVisualizations.initList()
|
||||
|
@ -81,6 +82,10 @@ export default class SpecialVisualizations {
|
|||
return []
|
||||
}
|
||||
|
||||
if (template["type"] !== undefined) {
|
||||
console.trace("Got a non-expanded template while constructing the specification")
|
||||
throw "Got a non-expanded template while constructing the specification"
|
||||
}
|
||||
const allKnownSpecials = extraMappings.concat(SpecialVisualizations.specialVisualizations)
|
||||
for (const knownSpecial of allKnownSpecials) {
|
||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||
|
@ -226,6 +231,7 @@ export default class SpecialVisualizations {
|
|||
|
||||
private static initList(): SpecialVisualization[] {
|
||||
const specialVisualizations: SpecialVisualization[] = [
|
||||
new QuestionViz(),
|
||||
new HistogramViz(),
|
||||
new StealViz(),
|
||||
new MinimapViz(),
|
||||
|
@ -956,7 +962,8 @@ export default class SpecialVisualizations {
|
|||
state,
|
||||
new UIEventSource<Record<string, string>>(e.feature.properties),
|
||||
e.args,
|
||||
e.feature
|
||||
e.feature,
|
||||
undefined
|
||||
)
|
||||
})
|
||||
return new Combine([new Title(s.funcName), s.docs, ...examples])
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
<ToSvelte construct={Svg.plus_ui}></ToSvelte>
|
||||
</MapControlButton>
|
||||
<MapControlButton on:click={() => mapproperties.zoom.update(z => z-1)}>
|
||||
<ToSvelte class="w-7 h-7 block" construct={Svg.min_ui}></ToSvelte>
|
||||
<ToSvelte construct={Svg.min_ui}></ToSvelte>
|
||||
</MapControlButton>
|
||||
<If condition={featureSwitches.featureSwitchGeolocation}>
|
||||
<MapControlButton>
|
||||
|
@ -79,9 +79,7 @@
|
|||
|
||||
<div class="absolute top-0 right-0 mt-4 mr-4">
|
||||
<If condition={state.featureSwitches.featureSwitchSearch}>
|
||||
<Geosearch bounds={state.mapProperties.bounds} layout={state.layout} location={state.mapProperties.location}
|
||||
{selectedElement} {selectedLayer}
|
||||
></Geosearch>
|
||||
<Geosearch bounds={state.mapProperties.bounds} {selectedElement} {selectedLayer}></Geosearch>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
|
@ -94,20 +92,20 @@
|
|||
<TabGroup>
|
||||
<TabList>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<Tr t={layout.title}/>
|
||||
<Tr t={layout.title} />
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>
|
||||
<Tr t={Translations.t.general.menu.filter}/>
|
||||
<Tr t={Translations.t.general.menu.filter} />
|
||||
</Tab>
|
||||
<Tab class={({selected}) => selected ? "tab-selected" : "tab-unselected"}>Tab 3</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel class="flex flex-col">
|
||||
<Tr t={layout.description}></Tr>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.general}/>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.general} />
|
||||
{#if layout.layers.some((l) => l.presets?.length > 0)}
|
||||
<If condition={state.featureSwitches.featureSwitchAddNew}>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.addNew}/>
|
||||
<Tr t={Translations.t.general.welcomeExplanation.addNew} />
|
||||
</If>
|
||||
{/if}
|
||||
|
||||
|
@ -168,12 +166,12 @@
|
|||
</If>
|
||||
|
||||
{#if $selectedElement !== undefined && $selectedLayer !== undefined}
|
||||
<div class="absolute top-0 right-0 w-screen h-screen" style="background-color: #00000088">
|
||||
<div class="absolute top-0 right-0 w-screen h-screen overflow-auto" style="background-color: #00000088">
|
||||
|
||||
<div class="w-full m-8 normal-background rounded overflow-auto">
|
||||
|
||||
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
|
||||
tags={$selectedElementTags} state={state}></SelectedElementView>
|
||||
<div class="flex flex-col m-4 sm:m-6 md:m-8 p-4 sm:p-6 md:m-8 normal-background rounded normal-background">
|
||||
|
||||
<SelectedElementView layer={$selectedLayer} selectedElement={$selectedElement}
|
||||
tags={$selectedElementTags} state={state}></SelectedElementView>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue