mapcomplete/UI/Popup/TagRenderingQuestion.ts
Tobias 15a9441b1d Fix question styling (but break click action)
This fixes the styling but breaks the click action. Clicking on the text itself also triggers the action, clicking on the blank sapce next to it changes the label but does not trigger the action.

Generally speaking the style is easier to apply to an element that wraps the in put and the label-text. This could be the label-tag or a different wrapper.
2021-01-21 06:03:34 +01:00

278 lines
10 KiB
TypeScript

import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import {InputElement} from "../Input/InputElement";
import {And, Tag, TagsFilter, TagUtils} from "../../Logic/Tags";
import ValidatedTextField from "../Input/ValidatedTextField";
import {FixedInputElement} from "../Input/FixedInputElement";
import {SubstitutedTranslation} from "../SpecialVisualizations";
import {RadioButton} from "../Input/RadioButton";
import {Utils} from "../../Utils";
import CheckBoxes from "../Input/Checkboxes";
import InputElementMap from "../Input/InputElementMap";
import {SaveButton} from "./SaveButton";
import State from "../../State";
import {Changes} from "../../Logic/Osm/Changes";
import {VariableUiElement} from "../Base/VariableUIElement";
import Translations from "../i18n/Translations";
import {FixedUiElement} from "../Base/FixedUiElement";
import {Translation} from "../i18n/Translation";
import Constants from "../../Models/Constants";
/**
* 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 UIElement {
private readonly _tags: UIEventSource<any>;
private _configuration: TagRenderingConfig;
private _saveButton: UIElement;
private _inputElement: InputElement<TagsFilter>;
private _cancelButton: UIElement;
private _appliedTags: UIElement;
private _question: UIElement;
constructor(tags: UIEventSource<any>,
configuration: TagRenderingConfig,
afterSave?: () => void,
cancelButton?: UIElement) {
super(tags);
this._tags = tags;
this._configuration = configuration;
this._cancelButton = cancelButton;
this._question = new SubstitutedTranslation(this._configuration.question, tags)
.SetClass("question-text");
if (configuration === undefined) {
throw "A question is needed for a question visualization"
}
this._inputElement = this.GenerateInputElement()
const self = this;
const save = () => {
console.log("Save clicked!")
const selection = self._inputElement.GetValue().data;
console.log("Selection is", selection)
if (selection) {
(State.state?.changes ?? new Changes())
.addTag(tags.data.id, selection, tags);
}
if (afterSave) {
afterSave();
}
}
this._saveButton = new SaveButton(this._inputElement.GetValue(), State.state?.osmConnection)
.onClick(save)
this._appliedTags = new VariableUiElement(
self._inputElement.GetValue().map(
(tags: TagsFilter) => {
const csCount = State.state?.osmConnection?.userDetails?.data?.csCount ?? 1000;
if (csCount < Constants.userJourney.tagsVisibleAt) {
return "";
}
if (tags === undefined) {
return Translations.t.general.noTagsSelected.SetClass("subtle").Render();
}
if (csCount < Constants.userJourney.tagsVisibleAndWikiLinked) {
const tagsStr = tags.asHumanString(false, true);
return new FixedUiElement(tagsStr).SetClass("subtle").Render();
}
return tags.asHumanString(true, true);
}
)
).AddClass("block")
}
private GenerateInputElement(): InputElement<TagsFilter> {
const ff = this.GenerateFreeform();
const self = this;
let mappings =
(this._configuration.mappings ?? []).map(mapping => self.GenerateMappingElement(mapping));
mappings = Utils.NoNull(mappings);
if (mappings.length == 0) {
return ff;
}
if (this._configuration.multiAnswer) {
return this.GenerateMultiAnswer(mappings, ff)
} else {
return new RadioButton(mappings, false)
}
}
private GenerateMultiAnswer(elements: InputElement<TagsFilter>[], freeformField: InputElement<TagsFilter>): InputElement<TagsFilter> {
const checkBoxes = new CheckBoxes(elements);
const inputEl = new InputElementMap<number[], TagsFilter>(
checkBoxes,
(t0, t1) => {
return t0?.isEquivalent(t1) ?? false
},
(indices) => {
if (indices.length === 0) {
return undefined;
}
const tags: TagsFilter[] = indices.map(i => elements[i].GetValue().data);
return TagUtils.FlattenMultiAnswer(tags);
},
(tags: TagsFilter) => {
// {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 (this._configuration.freeform?.key) {
freeformExtras = [...(presentTags[this._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[this._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(this._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 => {
const es = checkBoxes.GetValue();
const i = elements.length - 1;
const index = es.data.indexOf(i);
if (value === undefined) {
if (index >= 0) {
es.data.splice(index, 1);
es.ping();
}
} else if (index < 0) {
es.data.push(i);
es.ping();
}
});
return inputEl;
}
private GenerateMappingElement(mapping: {
if: TagsFilter,
then: Translation,
hideInAnswer: boolean | TagsFilter
}): InputElement<TagsFilter> {
if (mapping.hideInAnswer === true) {
return undefined;
}
if(typeof(mapping.hideInAnswer) !== "boolean" && mapping.hideInAnswer.matches(this._tags.data)){
return undefined;
}
return new FixedInputElement(
new SubstitutedTranslation(mapping.then, this._tags),
mapping.if,
(t0, t1) => t1.isEquivalent(t0));
}
private GenerateFreeform(): InputElement<TagsFilter> {
const freeform = this._configuration.freeform;
if (freeform === undefined) {
return undefined;
}
const pickString =
(string: any) => {
if (string === "" || string === undefined) {
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 textField = ValidatedTextField.InputForType(this._configuration.freeform.type, {
isValid: (str) => (str.length <= 255),
country: () => this._tags.data._country,
location: [this._tags.data._lat, this._tags.data._lon]
});
textField.GetValue().setData(this._tags.data[this._configuration.freeform.key]);
return new InputElementMap(
textField, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
pickString, toString
);
}
InnerRender(): string {
return new Combine([
this._question,
this._inputElement,
this._cancelButton,
this._saveButton,
this._appliedTags]
)
.SetClass("question")
.Render()
}
}