forked from MapComplete/MapComplete
Add the possibility to show all questions of a group as one + documentation update
This commit is contained in:
parent
3570cfbaa8
commit
519feaa54b
10 changed files with 140 additions and 34 deletions
|
@ -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
|
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)
|
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": "<h3>Technical questions</h3>The following questions are very technical!<br />{questions}
|
||||||
|
},
|
||||||
|
"freeform": {
|
||||||
|
"key": "questions",
|
||||||
|
"helperArgs": {
|
||||||
|
"showAllQuestions": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{ ... some tagrendering ... }
|
||||||
|
{ ... more tagrendering ...}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
Some hints
|
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!
|
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
|
Use the `filter`-functionality instead
|
||||||
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.
|
|
||||||
|
|
||||||
### Not reading the .JSON-specs
|
### Not reading the .JSON-specs
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,9 @@ export interface TagRenderingConfigJson {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id of the tagrendering, should be an unique string.
|
* 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,
|
id?: string,
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,12 @@ export default class TagRenderingConfig {
|
||||||
throw `Freeform.args is defined. This should probably be 'freeform.helperArgs' (at ${context})`
|
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) {
|
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!`
|
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){
|
if(txt.indexOf("{"+this.freeform.key+"}") >= 0){
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if(txt.indexOf("{"+this.freeform.key+":") >= 0){
|
||||||
|
continue
|
||||||
|
}
|
||||||
if(txt.indexOf("{canonical("+this.freeform.key+")") >= 0){
|
if(txt.indexOf("{canonical("+this.freeform.key+")") >= 0){
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -363,6 +375,9 @@ export default class TagRenderingConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(this.id === "questions"){
|
||||||
|
return this.render
|
||||||
|
}
|
||||||
|
|
||||||
if (this.freeform?.key === undefined) {
|
if (this.freeform?.key === undefined) {
|
||||||
return this.render;
|
return this.render;
|
||||||
|
|
|
@ -77,12 +77,6 @@ export default class WithContextLoader {
|
||||||
|
|
||||||
if (renderingJson["builtin"] !== undefined) {
|
if (renderingJson["builtin"] !== undefined) {
|
||||||
const renderingId = renderingJson["builtin"]
|
const renderingId = renderingJson["builtin"]
|
||||||
if (renderingId === "questions") {
|
|
||||||
const tr = new TagRenderingConfig("questions", context);
|
|
||||||
renderings.push(tr)
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sharedJson = WithContextLoader.getKnownTagRenderings(renderingId)
|
let sharedJson = WithContextLoader.getKnownTagRenderings(renderingId)
|
||||||
if (sharedJson === undefined) {
|
if (sharedJson === undefined) {
|
||||||
const keys = Array.from(SharedTagRenderings.SharedTagRenderingJson.keys());
|
const keys = Array.from(SharedTagRenderings.SharedTagRenderingJson.keys());
|
||||||
|
|
|
@ -187,10 +187,10 @@ class OpeningHoursTextField implements TextFieldDef {
|
||||||
return new OpeningHoursInput(value, prefix, postfix)
|
return new OpeningHoursInput(value, prefix, postfix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class ValidatedTextField {
|
export default class ValidatedTextField {
|
||||||
|
|
||||||
public static tpList: TextFieldDef[] = [
|
public static tpList: TextFieldDef[] = [
|
||||||
|
|
||||||
ValidatedTextField.tp(
|
ValidatedTextField.tp(
|
||||||
"string",
|
"string",
|
||||||
"A basic string"),
|
"A basic string"),
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {Utils} from "../../Utils";
|
||||||
import {SubstitutedTranslation} from "../SubstitutedTranslation";
|
import {SubstitutedTranslation} from "../SubstitutedTranslation";
|
||||||
import MoveWizard from "./MoveWizard";
|
import MoveWizard from "./MoveWizard";
|
||||||
import Toggle from "../Input/Toggle";
|
import Toggle from "../Input/Toggle";
|
||||||
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
|
||||||
export default class FeatureInfoBox extends ScrollableFullScreen {
|
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))
|
const allGroupNames = Utils.Dedup(layerConfig.tagRenderings.map(tr => tr.group))
|
||||||
if (State.state.featureSwitchUserbadge.data) {
|
if (State.state.featureSwitchUserbadge.data) {
|
||||||
|
const questionSpecs = layerConfig.tagRenderings.filter(tr => tr.id === "questions")
|
||||||
for (const groupName of allGroupNames) {
|
for (const groupName of allGroupNames) {
|
||||||
const questions = layerConfig.tagRenderings.filter(tr => tr.group === groupName)
|
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)
|
questionBoxes.set(groupName, questionBox)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -75,21 +83,22 @@ export default class FeatureInfoBox extends ScrollableFullScreen {
|
||||||
// This is a question box!
|
// This is a question box!
|
||||||
const questionBox = questionBoxes.get(tr.group)
|
const questionBox = questionBoxes.get(tr.group)
|
||||||
questionBoxes.delete(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", "", {
|
const renderedQuestion = new TagRenderingAnswer(tags, tr, tr.group + " questions", "", {
|
||||||
specialViz: new Map<string, BaseUIElement>([["questions", questionBox]])
|
specialViz: new Map<string, BaseUIElement>([["questions", questionBox]])
|
||||||
})
|
})
|
||||||
const possiblyHidden = new Toggle(
|
const possiblyHidden = new Toggle(
|
||||||
renderedQuestion,
|
renderedQuestion,
|
||||||
undefined,
|
undefined,
|
||||||
questionBox.currentQuestion.map(i => i !== undefined)
|
questionBox.restingQuestions.map(ls => ls?.length > 0)
|
||||||
)
|
)
|
||||||
renderingsForGroup.push(possiblyHidden)
|
renderingsForGroup.push(possiblyHidden)
|
||||||
}else{
|
} else {
|
||||||
renderingsForGroup.push(questionBox)
|
renderingsForGroup.push(questionBox)
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
let classes = innerClasses
|
let classes = innerClasses
|
||||||
let isHeader = renderingsForGroup.length === 0 && i > 0
|
let isHeader = renderingsForGroup.length === 0 && i > 0
|
||||||
|
|
|
@ -14,14 +14,19 @@ import Lazy from "../Base/Lazy";
|
||||||
*/
|
*/
|
||||||
export default class QuestionBox extends VariableUiElement {
|
export default class QuestionBox extends VariableUiElement {
|
||||||
public readonly skippedQuestions: UIEventSource<number[]>;
|
public readonly skippedQuestions: UIEventSource<number[]>;
|
||||||
public readonly currentQuestion: UIEventSource<number | undefined>;
|
public readonly restingQuestions: UIEventSource<BaseUIElement[]>;
|
||||||
|
|
||||||
constructor(options: { tagsSource: UIEventSource<any>, tagRenderings: TagRenderingConfig[], units: Unit[] }) {
|
constructor(options: {
|
||||||
|
tagsSource: UIEventSource<any>,
|
||||||
|
tagRenderings: TagRenderingConfig[], units: Unit[],
|
||||||
|
showAllQuestionsAtOnce?: boolean | UIEventSource<boolean>
|
||||||
|
}) {
|
||||||
|
|
||||||
const skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])
|
const skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])
|
||||||
|
|
||||||
const tagsSource = options.tagsSource
|
const tagsSource = options.tagsSource
|
||||||
const units = options.units
|
const units = options.units
|
||||||
|
options.showAllQuestionsAtOnce = options.showAllQuestionsAtOnce ?? false
|
||||||
const tagRenderings = options.tagRenderings
|
const tagRenderings = options.tagRenderings
|
||||||
.filter(tr => tr.question !== undefined)
|
.filter(tr => tr.question !== undefined)
|
||||||
.filter(tr => tr.question !== null)
|
.filter(tr => tr.question !== null)
|
||||||
|
@ -50,8 +55,7 @@ export default class QuestionBox extends VariableUiElement {
|
||||||
.onClick(() => {
|
.onClick(() => {
|
||||||
skippedQuestions.setData([]);
|
skippedQuestions.setData([]);
|
||||||
})
|
})
|
||||||
|
tagsSource.map(tags => {
|
||||||
const currentQuestion: UIEventSource<number | undefined> = tagsSource.map(tags => {
|
|
||||||
if (tags === undefined) {
|
if (tags === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -74,13 +78,40 @@ export default class QuestionBox extends VariableUiElement {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
return undefined; // The questions are depleted
|
return undefined; // The questions are depleted
|
||||||
|
}, [skippedQuestions]);
|
||||||
|
|
||||||
|
const questionsToAsk: UIEventSource<BaseUIElement[]> = 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])
|
}, [skippedQuestions])
|
||||||
|
|
||||||
|
super(questionsToAsk.map(allQuestions => {
|
||||||
super(currentQuestion.map(i => {
|
|
||||||
const els: BaseUIElement[] = []
|
const els: BaseUIElement[] = []
|
||||||
if (i !== undefined) {
|
if (options.showAllQuestionsAtOnce === true || options.showAllQuestionsAtOnce["data"]) {
|
||||||
els.push(tagRenderingQuestions[i])
|
els.push(...questionsToAsk.data)
|
||||||
|
} else {
|
||||||
|
els.push(allQuestions[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (skippedQuestions.data.length > 0) {
|
if (skippedQuestions.data.length > 0) {
|
||||||
|
@ -92,7 +123,7 @@ export default class QuestionBox extends VariableUiElement {
|
||||||
)
|
)
|
||||||
|
|
||||||
this.skippedQuestions = skippedQuestions;
|
this.skippedQuestions = skippedQuestions;
|
||||||
this.currentQuestion = currentQuestion
|
this.restingQuestions = questionsToAsk
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3384,6 +3384,12 @@
|
||||||
"render": {
|
"render": {
|
||||||
"en": "<h3>Technical questions</h3>The questions below are very technical. Feel free to ignore them<br/>{questions}",
|
"en": "<h3>Technical questions</h3>The questions below are very technical. Feel free to ignore them<br/>{questions}",
|
||||||
"nl": "<h3>Technische vragen</h3>De vragen hieronder zijn erg technisch - sla deze over indien je hier geen tijd voor hebt<br/>{questions}"
|
"nl": "<h3>Technische vragen</h3>De vragen hieronder zijn erg technisch - sla deze over indien je hier geen tijd voor hebt<br/>{questions}"
|
||||||
|
},
|
||||||
|
"freeform": {
|
||||||
|
"key": "questions",
|
||||||
|
"helperArgs": {
|
||||||
|
"showAllQuestions": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
{
|
{
|
||||||
|
"questions": {
|
||||||
|
"id": "questions"
|
||||||
|
},
|
||||||
"images": {
|
"images": {
|
||||||
"render": "{image_carousel()}{image_upload()}"
|
"render": "{image_carousel()}{image_upload()}"
|
||||||
},
|
},
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
color: var(--subtle-detail-color-contrast);
|
color: var(--subtle-detail-color-contrast);
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
font-size: larger;
|
font-size: larger !important;
|
||||||
overflow-wrap: initial;
|
overflow-wrap: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue