diff --git a/UI/Base/ModalRight.svelte b/UI/Base/ModalRight.svelte index b0a11a360..9ba2f4ec5 100644 --- a/UI/Base/ModalRight.svelte +++ b/UI/Base/ModalRight.svelte @@ -8,8 +8,8 @@ const dispatch = createEventDispatcher<{ close }>(); -
-
+
+
dispatch("close")}> diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index 601218a91..9a213b79f 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -49,33 +49,10 @@ export default abstract class BaseUIElement { return this } - public ScrollToTop() { - this._constructedHtmlElement?.scrollTo(0, 0) - } - - public ScrollIntoView(options?: { onlyIfPartiallyHidden?: boolean }) { + public ScrollIntoView() { if (this._constructedHtmlElement === undefined) { return } - let alignToTop = true - if (options?.onlyIfPartiallyHidden) { - // Is the element completely in the view? - const parentRect = Utils.findParentWithScrolling( - this._constructedHtmlElement.parentElement - ).getBoundingClientRect() - const elementRect = this._constructedHtmlElement.getBoundingClientRect() - - // Check if the element is within the vertical bounds of the parent element - const topIsVisible = elementRect.top >= parentRect.top - const bottomIsVisible = elementRect.bottom <= parentRect.bottom - const inView = topIsVisible && bottomIsVisible - if (inView) { - return - } - if (topIsVisible) { - alignToTop = false - } - } this._constructedHtmlElement?.scrollIntoView({ behavior: "smooth", block: "start", diff --git a/UI/BigComponents/SelectedElementView.svelte b/UI/BigComponents/SelectedElementView.svelte index 9bf83d432..5a5b732fc 100644 --- a/UI/BigComponents/SelectedElementView.svelte +++ b/UI/BigComponents/SelectedElementView.svelte @@ -1,67 +1,71 @@ {#if _tags._deleted === "yes"} - + {:else} -
-
- -

- -

+
+
+
-
- {#each layer.titleIcons as titleIconConfig} - {#if ( titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties(_tags) ?? true) && titleIconConfig.IsKnown(_tags)} -
- + +

+ +

+ +
+ {#each layer.titleIcons as titleIconConfig} + {#if (titleIconConfig.condition?.matchesProperties(_tags) ?? true) && (titleIconConfig.metacondition?.matchesProperties(_tags) ?? true) && titleIconConfig.IsKnown(_tags)} +
+ +
+ {/if} + {/each} +
+
- {/if} - {/each} -
- + state.selectedElement.setData(undefined)}/> +
+
+ {#each layer.tagRenderings as config (config.id)} + {#if (config.condition === undefined || config.condition.matchesProperties(_tags)) && (config.metacondition === undefined || config.metacondition.matchesProperties({..._tags, ..._metatags}))} + {#if config.IsKnown(_tags)} + + {/if} + {/if} + {/each} +
- -
- {#each layer.tagRenderings as config (config.id)} - {#if (config.condition === undefined || config.condition.matchesProperties(_tags)) && (config.metacondition === undefined || config.metacondition.matchesProperties({ ..._tags, ..._metatags }))} - {#if config.IsKnown(_tags)} - - {/if} - {/if} - {/each} -
- -
{/if} diff --git a/UI/InputElement/ValidatedInput.svelte b/UI/InputElement/ValidatedInput.svelte index e85dbaaf1..3a621802c 100644 --- a/UI/InputElement/ValidatedInput.svelte +++ b/UI/InputElement/ValidatedInput.svelte @@ -14,13 +14,16 @@ export let type: ValidatorType; export let feedback: UIEventSource | undefined = undefined; export let getCountry: () => string | undefined - + export let placeholder: string | Translation | undefined let validator : Validator = Validators.get(type) + let _placeholder = placeholder ?? validator?.getPlaceholder() ?? type + $: { // The type changed -> reset some values validator = Validators.get(type) _value.setData(value.data ?? "") feedback = feedback?.setData(validator?.getFeedback(_value.data, getCountry)); + _placeholder = placeholder ?? validator?.getPlaceholder() ?? type } onDestroy(_value.addCallbackAndRun(v => { @@ -50,10 +53,10 @@ {#if validator.textArea} - + {:else } - + {#if !$isValid} {/if} diff --git a/UI/InputElement/Validator.ts b/UI/InputElement/Validator.ts index 4736b28d3..110821971 100644 --- a/UI/InputElement/Validator.ts +++ b/UI/InputElement/Validator.ts @@ -52,6 +52,10 @@ export abstract class Validator { } } + public getPlaceholder(){ + return Translations.t.validation[this.name].description + } + public isValid(string: string, requestCountry?: () => string): boolean { return true } diff --git a/UI/Popup/AllTagsPanel.svelte b/UI/Popup/AllTagsPanel.svelte index e2e918bed..6baa5009b 100644 --- a/UI/Popup/AllTagsPanel.svelte +++ b/UI/Popup/AllTagsPanel.svelte @@ -63,9 +63,3 @@
- - diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts deleted file mode 100644 index 6675721b1..000000000 --- a/UI/Popup/EditableTagRendering.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import TagRenderingQuestion from "./TagRenderingQuestion" -import Translations from "../i18n/Translations" -import Combine from "../Base/Combine" -import TagRenderingAnswer from "./TagRenderingAnswer" -import Toggle from "../Input/Toggle" -import BaseUIElement from "../BaseUIElement" -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" -import { Unit } from "../../Models/Unit" -import Lazy from "../Base/Lazy" -import { FixedUiElement } from "../Base/FixedUiElement" -import { EditButton } from "./SaveButton" -import { UploadableTag } from "../../Logic/Tags/TagUtils" - -export default class EditableTagRendering extends Toggle { - constructor( - tags: UIEventSource, - configuration: TagRenderingConfig, - units: Unit[], - state, - options: { - editMode?: UIEventSource - innerElementClasses?: string - /* Classes applied _only_ on the rendered element, not on the question*/ - answerElementClasses?: string - /* Default will apply the tags to the relevant object, only use in special cases */ - createSaveButton?: (src: Store) => BaseUIElement - } - ) { - // The tagrendering is hidden if: - // - The answer is unknown. The questionbox will then show the question - // - There is a condition hiding the answer - const renderingIsShown = tags.map( - (tags) => - configuration.IsKnown(tags) && - (configuration?.condition?.matchesProperties(tags) ?? true) - ) - const editMode = options.editMode ?? new UIEventSource(false) - - super( - new Lazy(() => { - let rendering = EditableTagRendering.CreateRendering( - state, - tags, - configuration, - units, - editMode, - { - saveButtonConstructor: options?.createSaveButton, - answerElementClasses: options?.answerElementClasses, - } - ) - rendering.SetClass(options.innerElementClasses) - if (state?.featureSwitchIsDebugging?.data || state?.featureSwitchIsTesting?.data) { - rendering = new Combine([ - new FixedUiElement(configuration.id).SetClass("self-end subtle"), - rendering, - ]).SetClass("flex flex-col") - } - return rendering - }), - undefined, - renderingIsShown - ) - const self = this - editMode.addCallback((editing) => { - if (editing) { - self.ScrollIntoView() - } - }) - } - - private static CreateRendering( - state: any /*FeaturePipelineState*/, - tags: UIEventSource, - configuration: TagRenderingConfig, - units: Unit[], - editMode: UIEventSource, - options?: { - saveButtonConstructor?: (src: Store) => BaseUIElement - answerElementClasses?: string - } - ): BaseUIElement { - const answer: BaseUIElement = new TagRenderingAnswer(tags, configuration, state) - answer.SetClass("w-full") - let rendering = answer - - if (configuration.question !== undefined && (state?.featureSwitchUserbadge?.data ?? true)) { - // We have a question and editing is enabled - - const question = new Lazy( - () => - new TagRenderingQuestion(tags, configuration, state, { - units: units, - cancelButton: Translations.t.general.cancel - .Clone() - .SetClass("btn btn-secondary") - .onClick(() => { - editMode.setData(false) - }), - saveButtonConstr: options?.saveButtonConstructor, - afterSave: () => { - editMode.setData(false) - }, - }) - ) - - const answerWithEditButton = new Combine([ - answer, - new EditButton(state?.osmConnection, () => { - editMode.setData(true) - question.ScrollIntoView({ - onlyIfPartiallyHidden: true, - }) - }), - ]).SetClass("flex justify-between w-full " + (options?.answerElementClasses ?? "")) - rendering = new Toggle(question, answerWithEditButton, editMode) - } - return rendering - } -} diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts deleted file mode 100644 index b41477a64..000000000 --- a/UI/Popup/QuestionBox.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Store, UIEventSource } from "../../Logic/UIEventSource" -import TagRenderingQuestion from "./TagRenderingQuestion" -import Translations from "../i18n/Translations" -import Combine from "../Base/Combine" -import BaseUIElement from "../BaseUIElement" -import { VariableUiElement } from "../Base/VariableUIElement" -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" -import { Unit } from "../../Models/Unit" -import Lazy from "../Base/Lazy" -import { OsmServiceState } from "../../Logic/Osm/OsmConnection" - -/** - * @deprecated - * This element is getting stripped and is not used anymore - * Generates all the questions, one by one - */ -export default class QuestionBox extends VariableUiElement { - constructor( - state, - options: { - tagsSource: UIEventSource - tagRenderings: TagRenderingConfig[] - units: Unit[] - showAllQuestionsAtOnce?: boolean | Store - } - ) { - const skippedQuestions: UIEventSource = new UIEventSource([]) - - const tagsSource = options.tagsSource - const units = options.units - - let focus: () => void = () => {} - - const tagRenderingQuestions = tagRenderings.map( - (tagRendering, i) => - new Lazy( - () => - new TagRenderingQuestion(tagsSource, tagRendering, state, { - units: units, - afterSave: () => { - // We save and indicate progress by pinging and recalculating - skippedQuestions.ping() - focus() - }, - cancelButton: Translations.t.general.skip - .Clone() - .SetClass("btn btn-secondary") - .onClick(() => { - skippedQuestions.data.push(i) - skippedQuestions.ping() - focus() - }), - }) - ) - ) - - tagsSource.map( - (tags) => { - if (tags === undefined) { - return undefined - } - for (let i = 0; i < tagRenderingQuestions.length; i++) { - let tagRendering = tagRenderings[i] - - if (skippedQuestions.data.indexOf(i) >= 0) { - continue - } - if (tagRendering.IsKnown(tags)) { - continue - } - if (tagRendering.condition) { - if (!tagRendering.condition.matchesProperties(tags)) { - // Filtered away by the condition, so it is kindof known - continue - } - } - - // this value is NOT known - this is the question we have to show! - return i - } - return undefined // The questions are depleted - }, - [skippedQuestions] - ) - - const questionsToAsk: Store = tagsSource.map( - (tags) => { - if (tags === undefined) { - return [] - } - const qs = [] - for (let i = 0; i < tagRenderingQuestions.length; i++) { - let tagRendering = tagRenderings[i] - - if (skippedQuestions.data.indexOf(i) >= 0) { - continue - } - if (tagRendering.IsKnown(tags)) { - continue - } - if (tagRendering.condition && !tagRendering.condition.matchesProperties(tags)) { - // Filtered away by the condition, so it is kindof known - continue - } - - // this value is NOT known - this is the question we have to show! - qs.push(tagRenderingQuestions[i]) - } - return qs - }, - [skippedQuestions] - ) - - super( - questionsToAsk.map( - (allQuestions) => { - const apiState: OsmServiceState = state.osmConnection.apiIsOnline.data - if (apiState !== "online" && apiState !== "unknown") { - return undefined - } - const els: BaseUIElement[] = [] - if ( - options.showAllQuestionsAtOnce === true || - options.showAllQuestionsAtOnce["data"] - ) { - els.push(...questionsToAsk.data) - } else { - els.push(allQuestions[0]) - } - - return new Combine(els).SetClass("block mb-8") - }, - [state.osmConnection.apiIsOnline] - ) - ) - - focus = () => this.ScrollIntoView() - } -} diff --git a/UI/Popup/TagRendering/FreeformInput.svelte b/UI/Popup/TagRendering/FreeformInput.svelte index c49d841f1..fdee1d851 100644 --- a/UI/Popup/TagRendering/FreeformInput.svelte +++ b/UI/Popup/TagRendering/FreeformInput.svelte @@ -14,6 +14,11 @@ export let tags: UIEventSource>; export let feature: Feature = undefined; + + let placeholder = config.freeform?.placeholder + $: { + placeholder = config.freeform?.placeholder + } let feedback: UIEventSource = new UIEventSource(undefined); @@ -29,11 +34,11 @@ {#if config.freeform.inline} dispatch("selected")} - type={config.freeform.type} {value}> + type={config.freeform.type} {placeholder} {value}> {:else} dispatch("selected")} - type={config.freeform.type} {value}> + type={config.freeform.type} {placeholder} {value}> {/if} diff --git a/UI/Popup/TagRendering/TagRenderingEditable.svelte b/UI/Popup/TagRendering/TagRenderingEditable.svelte index f0480fff7..e6e0ea4a3 100644 --- a/UI/Popup/TagRendering/TagRenderingEditable.svelte +++ b/UI/Popup/TagRendering/TagRenderingEditable.svelte @@ -1,74 +1,90 @@
- {#if config.question} - {#if editMode} - - - - {:else} -
- - -
+ {#if config.question && $editingEnabled} + {#if editMode} + + + + {:else} +
+ + +
+ {/if} + {:else } + {/if} - {:else } - - {/if}
diff --git a/UI/Popup/TagRendering/TagRenderingMapping.svelte b/UI/Popup/TagRendering/TagRenderingMapping.svelte index a9fad70eb..7df906010 100644 --- a/UI/Popup/TagRendering/TagRenderingMapping.svelte +++ b/UI/Popup/TagRendering/TagRenderingMapping.svelte @@ -3,10 +3,8 @@ import SpecialTranslation from "./SpecialTranslation.svelte"; import type {SpecialVisualizationState} from "../../SpecialVisualization"; import type {Feature} from "geojson"; - import {Store, UIEventSource} from "../../../Logic/UIEventSource"; + import {UIEventSource} from "../../../Logic/UIEventSource"; import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; - import Locale from "../../i18n/Locale"; - import {onDestroy} from "svelte"; export let selectedElement: Feature export let tags: UIEventSource>; diff --git a/UI/Popup/TagRendering/TagRenderingQuestion.svelte b/UI/Popup/TagRendering/TagRenderingQuestion.svelte index 26fab817e..373f1013b 100644 --- a/UI/Popup/TagRendering/TagRenderingQuestion.svelte +++ b/UI/Popup/TagRendering/TagRenderingQuestion.svelte @@ -131,7 +131,6 @@
{/if} - {#if config.mappings?.length >= 8}
@@ -139,7 +138,6 @@
{/if} - {#if config.freeform?.key && !(mappings?.length > 0)} diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts deleted file mode 100644 index 75ef9fd84..000000000 --- a/UI/Popup/TagRenderingAnswer.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { UIEventSource } from "../../Logic/UIEventSource" -import { Utils } from "../../Utils" -import BaseUIElement from "../BaseUIElement" -import { VariableUiElement } from "../Base/VariableUIElement" -import { SubstitutedTranslation } from "../SubstitutedTranslation" -import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig" -import Combine from "../Base/Combine" -import Img from "../Base/Img" -import { SpecialVisualizationState } from "../SpecialVisualization" - -/*** - * Displays the correct value for a known tagrendering - */ -export default class TagRenderingAnswer extends VariableUiElement { - constructor( - tagsSource: UIEventSource, - configuration: TagRenderingConfig, - state: SpecialVisualizationState, - contentClasses: string = "", - contentStyle: string = "", - options?: { - specialViz: Map - } - ) { - if (configuration === undefined) { - throw "Trying to generate a tagRenderingAnswer without configuration..." - } - UIEventSource - if (tagsSource === undefined) { - throw "Trying to generate a tagRenderingAnswer without tagSource..." - } - super( - tagsSource - .map((tags) => { - if (tags === undefined) { - return undefined - } - - if (configuration.condition) { - if (!configuration.condition.matchesProperties(tags)) { - return undefined - } - } - - const trs = Utils.NoNull(configuration.GetRenderValues(tags)) - if (trs.length === 0) { - return undefined - } - - const valuesToRender: BaseUIElement[] = trs.map((tr) => { - const text = new SubstitutedTranslation( - tr.then, - tagsSource, - state, - options?.specialViz - ) - if (tr.icon === undefined) { - return text - } - return new Combine([ - new Img(tr.icon).SetClass("mapping-icon-" + (tr.iconClass ?? "small")), - text, - ]).SetClass("flex items-center") - }) - if (valuesToRender.length === 1) { - return valuesToRender[0] - } else if (valuesToRender.length > 1) { - return new Combine(valuesToRender).SetClass("flex flex-col") - } - return undefined - }) - .map((element: BaseUIElement) => - element?.SetClass(contentClasses)?.SetStyle(contentStyle) - ) - ) - - this.SetClass("flex items-center flex-row text-lg link-underline") - this.SetStyle("word-wrap: anywhere;") - } -} diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts deleted file mode 100644 index 25df9686a..000000000 --- a/UI/Popup/TagRenderingQuestion.ts +++ /dev/null @@ -1,634 +0,0 @@ -import { Store, Stores, UIEventSource } from "../../Logic/UIEventSource" -import Combine from "../Base/Combine" -import { InputElement, ReadonlyInputElement } from "../Input/InputElement" -import { FixedInputElement } from "../Input/FixedInputElement" -import { RadioButton } from "../Input/RadioButton" -import { Utils } from "../../Utils" -import CheckBoxes from "../Input/Checkboxes" -import InputElementMap from "../Input/InputElementMap" -import { SaveButton } from "./SaveButton" -import { VariableUiElement } from "../Base/VariableUIElement" -import Translations from "../i18n/Translations" -import { Translation } from "../i18n/Translation" -import { SubstitutedTranslation } from "../SubstitutedTranslation" -import { TagsFilter } from "../../Logic/Tags/TagsFilter" -import { Tag } from "../../Logic/Tags/Tag" -import { And } from "../../Logic/Tags/And" -import { TagUtils, UploadableTag } from "../../Logic/Tags/TagUtils" -import BaseUIElement from "../BaseUIElement" -import { DropDown } from "../Input/DropDown" -import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction" -import TagRenderingConfig, { Mapping } from "../../Models/ThemeConfig/TagRenderingConfig" -import { Unit } from "../../Models/Unit" -import VariableInputElement from "../Input/VariableInputElement" -import Toggle from "../Input/Toggle" -import FeaturePipelineState from "../../Logic/State/FeaturePipelineState" -import Title from "../Base/Title" -import { GeoOperations } from "../../Logic/GeoOperations" -import { SearchablePillsSelector } from "../Input/SearchableMappingsSelector" -import { OsmTags } from "../../Models/OsmFeature" - -/** - * @deprecated: getting stripped and getting ported - * Shows the question element. - * Note that the value _migh_ already be known, e.g. when selected or when changing the value - */ -export default class TagRenderingQuestion extends Combine { - constructor( - tags: UIEventSource & { id: string }>, - configuration: TagRenderingConfig, - state?: FeaturePipelineState, - options?: { - units?: Unit[] - afterSave?: () => void - cancelButton?: BaseUIElement - saveButtonConstr?: (src: Store) => BaseUIElement - } - ) { - const applicableMappingsSrc = Stores.ListStabilized( - tags.map((tags) => { - const applicableMappings: Mapping[] = [] - for (const mapping of configuration.mappings ?? []) { - if (mapping.hideInAnswer === true) { - continue - } - if (mapping.hideInAnswer === false || mapping.hideInAnswer === undefined) { - applicableMappings.push(mapping) - continue - } - const condition = mapping.hideInAnswer - const isShown = !condition.matchesProperties(tags) - if (isShown) { - applicableMappings.push(mapping) - } - } - return applicableMappings - }) - ) - - if (configuration === undefined) { - throw "A question is needed for a question visualization" - } - options = options ?? {} - const applicableUnit = (options.units ?? []).filter((unit) => - unit.isApplicableToKey(configuration.freeform?.key) - )[0] - const question = new Title( - new SubstitutedTranslation(configuration.question, tags, state).SetClass( - "question-text" - ), - 3 - ) - let questionHint = undefined - if (configuration.questionhint !== undefined) { - questionHint = new SubstitutedTranslation( - configuration.questionhint, - tags, - state - ).SetClass("font-bold subtle") - } - - const feedback = new UIEventSource(undefined) - const inputElement: ReadonlyInputElement = new VariableInputElement( - applicableMappingsSrc.map((applicableMappings) => { - return TagRenderingQuestion.GenerateInputElement( - state, - configuration, - applicableMappings, - applicableUnit, - tags, - feedback - ) - }) - ) - - if (options.saveButtonConstr === undefined) { - const save = () => { - const selection = TagUtils.FlattenMultiAnswer( - TagUtils.FlattenAnd(inputElement.GetValue().data, tags.data) - ) - if (selection) { - ;(state?.changes) - .applyAction( - new ChangeTagAction(tags.data.id, selection, tags.data, { - theme: state?.layoutToUse?.id ?? "unkown", - changeType: "answer", - }) - ) - .then((_) => { - console.log("Tagchanges applied") - }) - if (options.afterSave) { - options.afterSave() - } - } - } - - options.saveButtonConstr = (v) => new SaveButton(v, state?.osmConnection).onClick(save) - } - - const saveButton = new Combine([options.saveButtonConstr(inputElement.GetValue())]) - - super([ - question, - questionHint, - inputElement, - new VariableUiElement( - feedback.map((t) => - t - ?.SetStyle("padding-left: 0.75rem; padding-right: 0.75rem") - ?.SetClass("alert flex") - ) - ), - new Combine([options.cancelButton, saveButton]).SetClass( - "flex justify-end flex-wrap-reverse" - ), - new Toggle( - Translations.t.general.testing.SetClass("block alert"), - undefined, - state?.featureSwitchIsTesting - ), - ]) - - this.SetClass("question disable-links") - } - - private static GenerateInputElement( - state: FeaturePipelineState, - configuration: TagRenderingConfig, - applicableMappings: Mapping[], - applicableUnit: Unit, - tagsSource: UIEventSource, - feedback: UIEventSource - ): ReadonlyInputElement { - const hasImages = applicableMappings.findIndex((mapping) => mapping.icon !== undefined) >= 0 - let inputEls: InputElement[] - - const ifNotsPresent = applicableMappings.some((mapping) => mapping.ifnot !== undefined) - - if ( - applicableMappings.length > 8 && - (configuration.freeform?.type === undefined || - configuration.freeform?.type === "string") && - (!configuration.multiAnswer || configuration.freeform === undefined) - ) { - return TagRenderingQuestion.GenerateSearchableSelector( - state, - configuration, - applicableMappings, - tagsSource - ) - } - - // FreeForm input will be undefined if not present; will already contain a special input element if applicable - const ff = TagRenderingQuestion.GenerateFreeform( - state, - configuration, - applicableUnit, - tagsSource, - feedback - ) - - function allIfNotsExcept(excludeIndex: number): UploadableTag[] { - if (configuration.mappings === undefined || configuration.mappings.length === 0) { - return undefined - } - if (!ifNotsPresent) { - return [] - } - if (configuration.multiAnswer) { - // The multianswer will do the ifnot configuration themself - return [] - } - - const negativeMappings = [] - - for (let i = 0; i < applicableMappings.length; i++) { - const mapping = applicableMappings[i] - if (i === excludeIndex || mapping.ifnot === undefined) { - continue - } - negativeMappings.push(mapping.ifnot) - } - return Utils.NoNull(negativeMappings) - } - - if ( - applicableMappings.length < 8 || - configuration.multiAnswer || - (hasImages && applicableMappings.length < 16) || - ifNotsPresent - ) { - inputEls = (applicableMappings ?? []).map((mapping, i) => - TagRenderingQuestion.GenerateMappingElement( - state, - tagsSource, - mapping, - allIfNotsExcept(i) - ) - ) - inputEls = Utils.NoNull(inputEls) - } else { - const dropdown: InputElement = new DropDown( - "", - applicableMappings.map((mapping, i) => { - return { - value: new And([mapping.if, ...allIfNotsExcept(i)]), - shown: mapping.then.Subs(tagsSource.data), - } - }) - ) - - if (ff == undefined) { - return dropdown - } else { - inputEls = [dropdown] - } - } - - if (inputEls.length == 0) { - if (ff === undefined) { - throw "Error: could not generate a question: freeform and all mappings are undefined" - } - return ff - } - - if (ff) { - inputEls.push(ff) - } - - if (configuration.multiAnswer) { - return TagRenderingQuestion.GenerateMultiAnswer( - configuration, - inputEls, - ff, - applicableMappings.map((mp) => mp.ifnot) - ) - } else { - return new RadioButton(inputEls, { selectFirstAsDefault: false }) - } - } - - private static MappingToPillValue( - applicableMappings: Mapping[], - tagsSource: UIEventSource, - state: FeaturePipelineState - ): { - show: BaseUIElement - value: number - mainTerm: Record - searchTerms?: Record - original: Mapping - hasPriority?: Store - }[] { - const values: { - show: BaseUIElement - value: number - mainTerm: Record - searchTerms?: Record - original: Mapping - hasPriority?: Store - }[] = [] - const addIcons = applicableMappings.some((m) => m.icon !== undefined) - for (let i = 0; i < applicableMappings.length; i++) { - const mapping = applicableMappings[i] - const tr = mapping.then.Subs(tagsSource.data) - const patchedMapping = { - ...mapping, - iconClass: mapping.iconClass ?? `small-height`, - icon: mapping.icon ?? (addIcons ? "./assets/svg/none.svg" : undefined), - } - const fancy = TagRenderingQuestion.GenerateMappingContent( - patchedMapping, - tagsSource, - state - ).SetClass("normal-background") - values.push({ - show: fancy, - value: i, - mainTerm: tr.translations, - searchTerms: mapping.searchTerms, - original: mapping, - hasPriority: tagsSource.map((tags) => mapping.priorityIf?.matchesProperties(tags)), - }) - } - return values - } - - private static GenerateSearchableSelector( - state: FeaturePipelineState, - configuration: TagRenderingConfig, - applicableMappings: Mapping[], - tagsSource: UIEventSource, - options?: { - search: UIEventSource - } - ): InputElement { - const values = TagRenderingQuestion.MappingToPillValue( - applicableMappings, - tagsSource, - state - ) - - const searchValue: UIEventSource = - options?.search ?? new UIEventSource(undefined) - const ff = configuration.freeform - let onEmpty: BaseUIElement = undefined - if (ff !== undefined) { - onEmpty = new VariableUiElement( - searchValue.map((search) => configuration.render.Subs({ [ff.key]: search })) - ) - } - const mode = configuration.multiAnswer ? "select-many" : "select-one" - - const classes = "h-64 overflow-scroll" - const presetSearch = new SearchablePillsSelector(values, { - mode, - searchValue, - onNoMatches: onEmpty?.SetClass(classes).SetClass("flex justify-center items-center"), - searchAreaClass: classes, - }) - const fallbackTag = searchValue.map((s) => { - if (s === undefined || ff?.key === undefined) { - return undefined - } - return new Tag(ff.key, s) - }) - return new InputElementMap( - presetSearch, - (x0, x1) => { - if (x0 == x1) { - return true - } - if (x0 === undefined || x1 === undefined) { - return false - } - if (x0.and.length !== x1.and.length) { - return false - } - for (let i = 0; i < x0.and.length; i++) { - if (x1.and[i] != x0.and[i]) { - return false - } - } - return true - }, - (selected) => { - if ( - ff !== undefined && - searchValue.data?.length > 0 && - !presetSearch.someMatchFound.data - ) { - const t = fallbackTag.data - if (ff.addExtraTags) { - return new And([t, ...ff.addExtraTags]) - } - return new And([t]) - } - - if (selected === undefined || selected.length == 0) { - return undefined - } - - const tfs = Utils.NoNull( - applicableMappings.map((mapping, i) => { - if (selected.indexOf(i) >= 0) { - return mapping.if - } else { - return mapping.ifnot - } - }) - ) - return new And(tfs) - }, - (tf) => { - if (tf === undefined) { - return [] - } - const selected: number[] = [] - for (let i = 0; i < applicableMappings.length; i++) { - const mapping = applicableMappings[i] - if (tf.and.some((t) => mapping.if == t)) { - selected.push(i) - } - } - return selected - }, - [searchValue, presetSearch.someMatchFound] - ) - } - - private static GenerateMultiAnswer( - configuration: TagRenderingConfig, - elements: InputElement[], - freeformField: InputElement, - ifNotSelected: UploadableTag[] - ): InputElement { - const checkBoxes = new CheckBoxes(elements) - - const inputEl = new InputElementMap( - checkBoxes, - (t0, t1) => { - return t0?.shadows(t1) ?? false - }, - (indices) => { - if (indices.length === 0) { - return undefined - } - const tags: UploadableTag[] = indices.map((i) => elements[i].GetValue().data) - const oppositeTags: UploadableTag[] = [] - for (let i = 0; i < ifNotSelected.length; i++) { - if (indices.indexOf(i) >= 0) { - continue - } - const notSelected = ifNotSelected[i] - if (notSelected === undefined) { - continue - } - oppositeTags.push(notSelected) - } - tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)) - return TagUtils.FlattenMultiAnswer(tags) - }, - (tags: UploadableTag) => { - // {key --> values[]} - - const presentTags = TagUtils.SplitKeys([tags]) - const indices: number[] = [] - // We also collect the values that have to be added to the freeform field - let freeformExtras: string[] = [] - if (configuration.freeform?.key) { - freeformExtras = [...(presentTags[configuration.freeform.key] ?? [])] - } - - for (let j = 0; j < elements.length; j++) { - const inputElement = elements[j] - if (inputElement === freeformField) { - continue - } - const val = inputElement.GetValue() - const neededTags = TagUtils.SplitKeys([val.data]) - - // if every 'neededKeys'-value is present in presentKeys, we have a match and enable the index - if (TagUtils.AllKeysAreContained(presentTags, neededTags)) { - indices.push(j) - if (freeformExtras.length > 0) { - const freeformsToRemove: string[] = - neededTags[configuration.freeform.key] ?? [] - for (const toRm of freeformsToRemove) { - const i = freeformExtras.indexOf(toRm) - if (i >= 0) { - freeformExtras.splice(i, 1) - } - } - } - } - } - if (freeformField) { - if (freeformExtras.length > 0) { - freeformField - .GetValue() - .setData(new Tag(configuration.freeform.key, freeformExtras.join(";"))) - indices.push(elements.indexOf(freeformField)) - } else { - freeformField.GetValue().setData(undefined) - } - } - - return indices - }, - elements.map((el) => el.GetValue()) - ) - - freeformField?.GetValue()?.addCallbackAndRun((value) => { - // The list of indices of the selected elements - const es = checkBoxes.GetValue() - const i = elements.length - 1 - // The actual index of the freeform-element - const index = es.data.indexOf(i) - if (value === undefined) { - // No data is set in the freeform text field; so we delete the checkmark if it is selected - if (index >= 0) { - es.data.splice(index, 1) - es.ping() - } - } else if (index < 0) { - // There is data defined in the checkmark, but the checkmark isn't checked, so we check it - // This is of course because the data changed - es.data.push(i) - es.ping() - } - }) - - return inputEl - } - - /** - * Generates a (Fixed) input element for this mapping. - * Note that the mapping might hide itself if the condition is not met anymore. - * - * Returns: [the element itself, the value to select if not selected. The contents of this UIEventSource might swap to undefined if the conditions to show the answer are unmet] - */ - private static GenerateMappingElement( - state, - tagsSource: UIEventSource, - mapping: Mapping, - ifNot?: UploadableTag[] - ): InputElement { - let tagging: UploadableTag = mapping.if - if (ifNot !== undefined) { - tagging = new And([mapping.if, ...ifNot]) - } - if (mapping.addExtraTags) { - tagging = new And([tagging, ...mapping.addExtraTags]) - } - - return new FixedInputElement( - TagRenderingQuestion.GenerateMappingContent(mapping, tagsSource, state), - tagging, - (t0, t1) => t1.shadows(t0) - ) - } - - private static GenerateMappingContent( - mapping: Mapping, - tagsSource: UIEventSource, - state: FeaturePipelineState - ): BaseUIElement { - return undefined - } - - private static GenerateFreeform( - state: FeaturePipelineState, - configuration: TagRenderingConfig, - applicableUnit: Unit, - tags: UIEventSource, - feedback: UIEventSource - ): InputElement { - const freeform = configuration.freeform - if (freeform === undefined) { - return undefined - } - - const pickString = (string: any) => { - if (string === "" || string === undefined) { - return undefined - } - if (string.length >= 255) { - return undefined - } - - const tag = new Tag(freeform.key, string) - - if (freeform.addExtraTags === undefined) { - return tag - } - return new And([tag, ...freeform.addExtraTags]) - } - - const toString = (tag) => { - if (tag instanceof And) { - for (const subtag of tag.and) { - if (subtag instanceof Tag && subtag.key === freeform.key) { - return subtag.value - } - } - - return undefined - } else if (tag instanceof Tag) { - return tag.value - } - return undefined - } - - const tagsData = tags.data - const feature = state?.allElements?.ContainingFeatures?.get(tagsData.id) - const center = feature != undefined ? GeoOperations.centerpointCoordinates(feature) : [0, 0] - const input: InputElement = ValidatedTextField.ForType( - configuration.freeform.type - )?.ConstructInputElement({ - country: () => tagsData._country, - location: [center[1], center[0]], - mapBackgroundLayer: state?.backgroundLayer, - unit: applicableUnit, - args: configuration.freeform.helperArgs, - feature, - placeholder: configuration.freeform.placeholder, - feedback, - }) - - // Add a length check - input?.GetValue().addCallbackD((v: string | undefined) => { - if (v?.length >= 255) { - feedback.setData(Translations.t.validation.tooLong.Subs({ count: v.length })) - } - }) - - return new InputElementMap( - input, - (a, b) => a === b || (a?.shadows(b) ?? false), - pickString, - toString - ) - } -} diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index e7daccd83..ef8d120de 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -1,59 +1,55 @@ import Combine from "./Base/Combine" -import { FixedUiElement } from "./Base/FixedUiElement" +import {FixedUiElement} from "./Base/FixedUiElement" import BaseUIElement from "./BaseUIElement" import Title from "./Base/Title" import Table from "./Base/Table" -import { - RenderingSpecification, - SpecialVisualization, - SpecialVisualizationState, -} from "./SpecialVisualization" -import { HistogramViz } from "./Popup/HistogramViz" -import { MinimapViz } from "./Popup/MinimapViz" -import { ShareLinkViz } from "./Popup/ShareLinkViz" -import { UploadToOsmViz } from "./Popup/UploadToOsmViz" -import { MultiApplyViz } from "./Popup/MultiApplyViz" -import { AddNoteCommentViz } from "./Popup/AddNoteCommentViz" -import { PlantNetDetectionViz } from "./Popup/PlantNetDetectionViz" -import { ConflateButton, ImportPointButton, ImportWayButton } from "./Popup/ImportButton" +import {RenderingSpecification, SpecialVisualization, SpecialVisualizationState,} from "./SpecialVisualization" +import {HistogramViz} from "./Popup/HistogramViz" +import {MinimapViz} from "./Popup/MinimapViz" +import {ShareLinkViz} from "./Popup/ShareLinkViz" +import {UploadToOsmViz} from "./Popup/UploadToOsmViz" +import {MultiApplyViz} from "./Popup/MultiApplyViz" +import {AddNoteCommentViz} from "./Popup/AddNoteCommentViz" +import {PlantNetDetectionViz} from "./Popup/PlantNetDetectionViz" +import {ConflateButton, ImportPointButton, ImportWayButton} from "./Popup/ImportButton" import TagApplyButton from "./Popup/TagApplyButton" -import { CloseNoteButton } from "./Popup/CloseNoteButton" -import { MapillaryLinkVis } from "./Popup/MapillaryLinkVis" -import { Store, Stores, UIEventSource } from "../Logic/UIEventSource" +import {CloseNoteButton} from "./Popup/CloseNoteButton" +import {MapillaryLinkVis} from "./Popup/MapillaryLinkVis" +import {Store, Stores, UIEventSource} from "../Logic/UIEventSource" import AllTagsPanel from "./Popup/AllTagsPanel.svelte" import AllImageProviders from "../Logic/ImageProviders/AllImageProviders" -import { ImageCarousel } from "./Image/ImageCarousel" -import { ImageUploadFlow } from "./Image/ImageUploadFlow" -import { VariableUiElement } from "./Base/VariableUIElement" -import { Utils } from "../Utils" -import Wikidata, { WikidataResponse } from "../Logic/Web/Wikidata" -import { Translation } from "./i18n/Translation" +import {ImageCarousel} from "./Image/ImageCarousel" +import {ImageUploadFlow} from "./Image/ImageUploadFlow" +import {VariableUiElement} from "./Base/VariableUIElement" +import {Utils} from "../Utils" +import Wikidata, {WikidataResponse} from "../Logic/Web/Wikidata" +import {Translation} from "./i18n/Translation" import Translations from "./i18n/Translations" import ReviewForm from "./Reviews/ReviewForm" import ReviewElement from "./Reviews/ReviewElement" import OpeningHoursVisualization from "./OpeningHours/OpeningHoursVisualization" import LiveQueryHandler from "../Logic/Web/LiveQueryHandler" -import { SubtleButton } from "./Base/SubtleButton" +import {SubtleButton} from "./Base/SubtleButton" import Svg from "../Svg" -import { OpenIdEditor, OpenJosm } from "./BigComponents/CopyrightPanel" +import {OpenIdEditor, OpenJosm} from "./BigComponents/CopyrightPanel" import Hash from "../Logic/Web/Hash" import NoteCommentElement from "./Popup/NoteCommentElement" import ImgurUploader from "../Logic/ImageProviders/ImgurUploader" import FileSelectorButton from "./Input/FileSelectorButton" -import { LoginToggle } from "./Popup/LoginButton" +import {LoginToggle} from "./Popup/LoginButton" import Toggle from "./Input/Toggle" -import { SubstitutedTranslation } from "./SubstitutedTranslation" +import {SubstitutedTranslation} from "./SubstitutedTranslation" import List from "./Base/List" import StatisticsPanel from "./BigComponents/StatisticsPanel" import AutoApplyButton from "./Popup/AutoApplyButton" -import { LanguageElement } from "./Popup/LanguageElement" +import {LanguageElement} from "./Popup/LanguageElement" import FeatureReviews from "../Logic/Web/MangroveReviews" import Maproulette from "../Logic/Maproulette" import SvelteUIElement from "./Base/SvelteUIElement" -import { BBoxFeatureSourceForLayer } from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" +import {BBoxFeatureSourceForLayer} from "../Logic/FeatureSource/Sources/TouchesBboxFeatureSource" import QuestionViz from "./Popup/QuestionViz" -import { Feature, Point } from "geojson" -import { GeoOperations } from "../Logic/GeoOperations" +import {Feature, Point} from "geojson" +import {GeoOperations} from "../Logic/GeoOperations" import CreateNewNote from "./Popup/CreateNewNote.svelte" import AddNewPoint from "./Popup/AddNewPoint/AddNewPoint.svelte" import UserProfile from "./BigComponents/UserProfile.svelte" @@ -61,25 +57,21 @@ import LanguagePicker from "./LanguagePicker" import Link from "./Base/Link" import LayerConfig from "../Models/ThemeConfig/LayerConfig" import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig" -import EditableTagRendering from "./Popup/EditableTagRendering" -import NearbyImages, { - NearbyImageOptions, - P4CPicture, - SelectOneNearbyImage, -} from "./Popup/NearbyImages" -import { Tag } from "../Logic/Tags/Tag" +import NearbyImages, {NearbyImageOptions, P4CPicture, SelectOneNearbyImage,} from "./Popup/NearbyImages" +import {Tag} from "../Logic/Tags/Tag" import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction" -import { And } from "../Logic/Tags/And" -import { SaveButton } from "./Popup/SaveButton" +import {And} from "../Logic/Tags/And" +import {SaveButton} from "./Popup/SaveButton" import Lazy from "./Base/Lazy" -import { CheckBox } from "./Input/Checkboxes" +import {CheckBox} from "./Input/Checkboxes" import Slider from "./Input/Slider" import DeleteWizard from "./Popup/DeleteWizard" -import { OsmId, OsmTags, WayId } from "../Models/OsmFeature" +import {OsmId, OsmTags, WayId} from "../Models/OsmFeature" import MoveWizard from "./Popup/MoveWizard" import SplitRoadWizard from "./Popup/SplitRoadWizard" -import { ExportAsGpxViz } from "./Popup/ExportAsGpxViz" +import {ExportAsGpxViz} from "./Popup/ExportAsGpxViz" import WikipediaPanel from "./Wikipedia/WikipediaPanel.svelte" +import TagRenderingEditable from "./Popup/TagRendering/TagRenderingEditable.svelte"; class NearbyImageVis implements SpecialVisualization { // Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests @@ -250,22 +242,22 @@ class StealViz implements SpecialVisualization { return undefined } const otherTags = state.featureProperties.getStore(featureId) + const otherFeature = state.indexedFeatures.featuresById.data.get(featureId); const elements: BaseUIElement[] = [] for (const [layer, tagRendering] of tagRenderings) { - const el = new EditableTagRendering( - otherTags, - tagRendering, - layer.units, + elements.push(new SvelteUIElement(TagRenderingEditable, { + config: tagRendering, + tags: otherTags, + selectedElement: otherFeature, state, - {} - ) - elements.push(el) + layer + })) } if (elements.length === 1) { return elements[0] } return new Combine(elements).SetClass("flex flex-col") - }) + }, [state.indexedFeatures.featuresById]) ) } @@ -638,7 +630,7 @@ export default class SpecialVisualizations { ], example: "`{wikipedia()}` is a basic example, `{wikipedia(name:etymology:wikidata)}` to show the wikipedia page of whom the feature was named after. Also remember that these can be styled, e.g. `{wikipedia():max-height: 10rem}` to limit the height", - constr: (_, tagsSource, args, feature, layer) => { + constr: (_, tagsSource, args) => { const keys = args[0].split(";").map((k) => k.trim()) const wikiIds: Store = tagsSource.map((tags) => { const key = keys.find((k) => tags[k] !== undefined && tags[k] !== "") diff --git a/Utils.ts b/Utils.ts index aef5d8d92..d57c4bc9f 100644 --- a/Utils.ts +++ b/Utils.ts @@ -1,4 +1,5 @@ import colors from "./assets/colors.json" +import {HTMLElement} from "node-html-parser"; export class Utils { /** @@ -1365,7 +1366,25 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be d.setUTCMinutes(0) } - public static findParentWithScrolling(element: HTMLElement): HTMLElement { + public static scrollIntoView(element: HTMLBaseElement){ + console.log("Scrolling into view:", element) + // Is the element completely in the view? + const parentRect = Utils.findParentWithScrolling( + element + ).getBoundingClientRect() + const elementRect = element.getBoundingClientRect() + + // Check if the element is within the vertical bounds of the parent element + const topIsVisible = elementRect.top >= parentRect.top + const bottomIsVisible = elementRect.bottom <= parentRect.bottom + const inView = topIsVisible && bottomIsVisible + if (inView) { + return + } + console.log("Actually scrolling...") + element.scrollIntoView({behavior: "smooth", block: "nearest"}) + } + public static findParentWithScrolling(element: HTMLBaseElement): HTMLBaseElement { // Check if the element itself has scrolling if (element.scrollHeight > element.clientHeight) { return element @@ -1377,7 +1396,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be } // If the element has a parent, repeat the process for the parent element - return Utils.findParentWithScrolling(element.parentElement) + return Utils.findParentWithScrolling( element.parentElement) } /**