From 519feaa54b8e3c673e9d718578f86a7dafe8f76d Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Fri, 12 Nov 2021 01:44:13 +0100 Subject: [PATCH] Add the possibility to show all questions of a group as one + documentation update --- Docs/Making_Your_Own_Theme.md | 66 ++++++++++++++++--- .../Json/TagRenderingConfigJson.ts | 4 +- Models/ThemeConfig/TagRenderingConfig.ts | 15 +++++ Models/ThemeConfig/WithContextLoader.ts | 6 -- UI/Input/ValidatedTextField.ts | 2 +- UI/Popup/FeatureInfoBox.ts | 21 ++++-- UI/Popup/QuestionBox.ts | 49 +++++++++++--- .../charging_station/charging_station.json | 6 ++ assets/tagRenderings/questions.json | 3 + css/tagrendering.css | 2 +- 10 files changed, 140 insertions(+), 34 deletions(-) diff --git a/Docs/Making_Your_Own_Theme.md b/Docs/Making_Your_Own_Theme.md index e07fea8df..9b206e445 100644 --- a/Docs/Making_Your_Own_Theme.md +++ b/Docs/Making_Your_Own_Theme.md @@ -112,6 +112,61 @@ A JSON-schema file is available in Docs/Schemas - use LayoutConfig.schema.json t There are few tags available that are calculated for convenience - e.g. the country an object is located at. [An overview of all these metatags is available here](Docs/CalculatedTags.md) + +### TagRendering groups + +A tagRendering can have a `group`-attribute, which acts as a tag. +All tagRenderings with the same group name will be rendered together, in the same order as they were defined. + +For example, if the defined tagrenderings have groups `A A B A A B B B`, the group order is `A B` and first all tagrenderings from group A will be rendered (thus numbers 0, 1, 3 and 4) followed by the question box for this group. +Then, all the tagRenderings for group B will be shown, thus number 2, 5, 6 and 7, again followed by their questionbox. + +Additionally, every tagrendering will receive a the groupname as class in the HTML, which can be used to hook up custom CSS. + +If no group tag is given, the group is `` (empty string) + +### Deciding the questions position + +By default, the questions are shown just beneath their group. + +To override this behaviour, one can add a tagrendering with id `questions` to move the questions up. + +To add a title to the questions, one can add a `render` and a condition. + +To change the behaviour of the questionbox to show _all_ questions at once, one can use a helperArgs in the freeform field with option `showAllQuestions`. + +For example, to show the questions on top, use: + +``` +"tagRenderings": [ + { "id": "questions" } + { ... some tagrendering ... } + { ... more tagrendering ...} +] +``` + +To show _all_ the questions of a group at once in the middle of the tagrenderings, with a header, use: + +``` +"tagRenderings": [ + { + "id": "questions" , + "group": "groupname", + "render": { + "en": "

Technical questions

The following questions are very technical!
{questions} + }, + "freeform": { + "key": "questions", + "helperArgs": { + "showAllQuestions": true + } + } + } + { ... some tagrendering ... } + { ... more tagrendering ...} +] +``` + Some hints ------------ @@ -175,16 +230,7 @@ Instead, make one layer for one kind of object and change the icon based on attr Using layers as filters - this doesn't work! -_All_ data is downloaded in one go and cached locally first. The layer selection (bottom left of the live app) then -selects _anything_ that matches the criteria. This match is then passed of to the rendering layer, which selects the -layer independently. This means that a feature can show up, even if it's layer is unselected! - -For example, in the [cyclofix-theme](https://mapcomplete.osm.org/cyclofix), there is the layer with _bike-wash_ for do -it yourself bikecleaning - points marked with `service:bicycle:cleaning`. However, a bicycle repair shop can offer this -service too! - -If all the layers are deselected except the bike wash layer, a shop having this tag will still match and will still show -up as shop. +Use the `filter`-functionality instead ### Not reading the .JSON-specs diff --git a/Models/ThemeConfig/Json/TagRenderingConfigJson.ts b/Models/ThemeConfig/Json/TagRenderingConfigJson.ts index e00bf8487..75f91018c 100644 --- a/Models/ThemeConfig/Json/TagRenderingConfigJson.ts +++ b/Models/ThemeConfig/Json/TagRenderingConfigJson.ts @@ -8,7 +8,9 @@ export interface TagRenderingConfigJson { /** * The id of the tagrendering, should be an unique string. - * Used to keep the translations in sync. Only used in the tagRenderings-array of a layerConfig, not requered otherwise + * Used to keep the translations in sync. Only used in the tagRenderings-array of a layerConfig, not requered otherwise. + * + * Use 'questions' to trigger the question box of this group (if a group is defined) */ id?: string, diff --git a/Models/ThemeConfig/TagRenderingConfig.ts b/Models/ThemeConfig/TagRenderingConfig.ts index e9d74f377..c1374b7ee 100644 --- a/Models/ThemeConfig/TagRenderingConfig.ts +++ b/Models/ThemeConfig/TagRenderingConfig.ts @@ -104,6 +104,12 @@ export default class TagRenderingConfig { throw `Freeform.args is defined. This should probably be 'freeform.helperArgs' (at ${context})` } + + if(json.freeform.key === "questions"){ + if(this.id !== "questions"){ + throw `If you use a freeform key 'questions', the ID must be 'questions' too to trigger the special behaviour. The current id is '${this.id}' (at ${context})` + } + } if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) { @@ -186,6 +192,9 @@ export default class TagRenderingConfig { throw `${context}: The rendering for language ${ln} does not contain {questions}. This is a bug, as this rendering should include exactly this to trigger those questions to be shown!` } + if(this.freeform?.key !== undefined && this.freeform?.key !== "questions"){ + throw `${context}: If the ID is questions to trigger a question box, the only valid freeform value is 'questions' as well. Set freeform to questions or remove the freeform all together` + } } @@ -201,6 +210,9 @@ export default class TagRenderingConfig { if(txt.indexOf("{"+this.freeform.key+"}") >= 0){ continue } + if(txt.indexOf("{"+this.freeform.key+":") >= 0){ + continue + } if(txt.indexOf("{canonical("+this.freeform.key+")") >= 0){ continue } @@ -363,6 +375,9 @@ export default class TagRenderingConfig { } } + if(this.id === "questions"){ + return this.render + } if (this.freeform?.key === undefined) { return this.render; diff --git a/Models/ThemeConfig/WithContextLoader.ts b/Models/ThemeConfig/WithContextLoader.ts index 4290d73ef..ed9e2580a 100644 --- a/Models/ThemeConfig/WithContextLoader.ts +++ b/Models/ThemeConfig/WithContextLoader.ts @@ -77,12 +77,6 @@ export default class WithContextLoader { if (renderingJson["builtin"] !== undefined) { const renderingId = renderingJson["builtin"] - if (renderingId === "questions") { - const tr = new TagRenderingConfig("questions", context); - renderings.push(tr) - continue; - } - let sharedJson = WithContextLoader.getKnownTagRenderings(renderingId) if (sharedJson === undefined) { const keys = Array.from(SharedTagRenderings.SharedTagRenderingJson.keys()); diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 16e222358..181cd5ea6 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -187,10 +187,10 @@ class OpeningHoursTextField implements TextFieldDef { return new OpeningHoursInput(value, prefix, postfix) } } - export default class ValidatedTextField { public static tpList: TextFieldDef[] = [ + ValidatedTextField.tp( "string", "A basic string"), diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index f2e018dc1..b6d79abd5 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -18,6 +18,7 @@ import {Utils} from "../../Utils"; import {SubstitutedTranslation} from "../SubstitutedTranslation"; import MoveWizard from "./MoveWizard"; import Toggle from "../Input/Toggle"; +import {FixedUiElement} from "../Base/FixedUiElement"; export default class FeatureInfoBox extends ScrollableFullScreen { @@ -56,9 +57,16 @@ export default class FeatureInfoBox extends ScrollableFullScreen { const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map(tr => tr.group)) if (State.state.featureSwitchUserbadge.data) { + const questionSpecs = layerConfig.tagRenderings.filter(tr => tr.id === "questions") for (const groupName of allGroupNames) { const questions = layerConfig.tagRenderings.filter(tr => tr.group === groupName) - const questionBox = new QuestionBox({tagsSource: tags, tagRenderings: questions, units:layerConfig.units}); + const questionSpec = questionSpecs.filter(tr => tr.group === groupName)[0] + const questionBox = new QuestionBox({ + tagsSource: tags, + tagRenderings: questions, + units: layerConfig.units, + showAllQuestionsAtOnce: questionSpec?.freeform?.helperArgs["showAllQuestions"] ?? State.state.featureSwitchShowAllQuestions + }); questionBoxes.set(groupName, questionBox) } } @@ -75,21 +83,22 @@ export default class FeatureInfoBox extends ScrollableFullScreen { // This is a question box! const questionBox = questionBoxes.get(tr.group) questionBoxes.delete(tr.group) - - if(tr.render !== undefined){ + + if (tr.render !== undefined) { + questionBox.SetClass("text-sm") const renderedQuestion = new TagRenderingAnswer(tags, tr, tr.group + " questions", "", { specialViz: new Map([["questions", questionBox]]) }) const possiblyHidden = new Toggle( renderedQuestion, undefined, - questionBox.currentQuestion.map(i => i !== undefined) + questionBox.restingQuestions.map(ls => ls?.length > 0) ) renderingsForGroup.push(possiblyHidden) - }else{ + } else { renderingsForGroup.push(questionBox) } - + } else { let classes = innerClasses let isHeader = renderingsForGroup.length === 0 && i > 0 diff --git a/UI/Popup/QuestionBox.ts b/UI/Popup/QuestionBox.ts index c38ce7806..5a857eef2 100644 --- a/UI/Popup/QuestionBox.ts +++ b/UI/Popup/QuestionBox.ts @@ -14,14 +14,19 @@ import Lazy from "../Base/Lazy"; */ export default class QuestionBox extends VariableUiElement { public readonly skippedQuestions: UIEventSource; - public readonly currentQuestion: UIEventSource; + public readonly restingQuestions: UIEventSource; - constructor(options: { tagsSource: UIEventSource, tagRenderings: TagRenderingConfig[], units: Unit[] }) { + constructor(options: { + tagsSource: UIEventSource, + tagRenderings: TagRenderingConfig[], units: Unit[], + showAllQuestionsAtOnce?: boolean | UIEventSource + }) { const skippedQuestions: UIEventSource = new UIEventSource([]) const tagsSource = options.tagsSource const units = options.units + options.showAllQuestionsAtOnce = options.showAllQuestionsAtOnce ?? false const tagRenderings = options.tagRenderings .filter(tr => tr.question !== undefined) .filter(tr => tr.question !== null) @@ -50,8 +55,7 @@ export default class QuestionBox extends VariableUiElement { .onClick(() => { skippedQuestions.setData([]); }) - - const currentQuestion: UIEventSource = tagsSource.map(tags => { + tagsSource.map(tags => { if (tags === undefined) { return undefined; } @@ -74,13 +78,40 @@ export default class QuestionBox extends VariableUiElement { return i } return undefined; // The questions are depleted + }, [skippedQuestions]); + + const questionsToAsk: UIEventSource = 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(currentQuestion.map(i => { + super(questionsToAsk.map(allQuestions => { const els: BaseUIElement[] = [] - if (i !== undefined) { - els.push(tagRenderingQuestions[i]) + if (options.showAllQuestionsAtOnce === true || options.showAllQuestionsAtOnce["data"]) { + els.push(...questionsToAsk.data) + } else { + els.push(allQuestions[0]) } if (skippedQuestions.data.length > 0) { @@ -92,7 +123,7 @@ export default class QuestionBox extends VariableUiElement { ) this.skippedQuestions = skippedQuestions; - this.currentQuestion = currentQuestion + this.restingQuestions = questionsToAsk } diff --git a/assets/layers/charging_station/charging_station.json b/assets/layers/charging_station/charging_station.json index 05c840ab6..b96c4ae41 100644 --- a/assets/layers/charging_station/charging_station.json +++ b/assets/layers/charging_station/charging_station.json @@ -3384,6 +3384,12 @@ "render": { "en": "

Technical questions

The questions below are very technical. Feel free to ignore them
{questions}", "nl": "

Technische vragen

De vragen hieronder zijn erg technisch - sla deze over indien je hier geen tijd voor hebt
{questions}" + }, + "freeform": { + "key": "questions", + "helperArgs": { + "showAllQuestions": true + } } } ], diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index 16924328e..4415089c4 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -1,4 +1,7 @@ { + "questions": { + "id": "questions" + }, "images": { "render": "{image_carousel()}{image_upload()}" }, diff --git a/css/tagrendering.css b/css/tagrendering.css index 7b675e563..fa8106f58 100644 --- a/css/tagrendering.css +++ b/css/tagrendering.css @@ -11,7 +11,7 @@ color: var(--subtle-detail-color-contrast); padding: 1em; border-radius: 1em; - font-size: larger; + font-size: larger !important; overflow-wrap: initial; }