Add option to show text field inline with the rendering; add option to fill out a default value
This commit is contained in:
parent
29ea0ac925
commit
6e3c39e475
10 changed files with 123 additions and 23 deletions
|
@ -26,6 +26,8 @@ export default class TagRenderingConfig {
|
||||||
readonly key: string,
|
readonly key: string,
|
||||||
readonly type: string,
|
readonly type: string,
|
||||||
readonly addExtraTags: TagsFilter[];
|
readonly addExtraTags: TagsFilter[];
|
||||||
|
readonly inline: boolean,
|
||||||
|
readonly default?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
readonly multiAnswer: boolean;
|
readonly multiAnswer: boolean;
|
||||||
|
@ -73,6 +75,8 @@ export default class TagRenderingConfig {
|
||||||
type: json.freeform.type ?? "string",
|
type: json.freeform.type ?? "string",
|
||||||
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
|
addExtraTags: json.freeform.addExtraTags?.map((tg, i) =>
|
||||||
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [],
|
FromJSON.Tag(tg, `${context}.extratag[${i}]`)) ?? [],
|
||||||
|
inline: json.freeform.inline ?? false,
|
||||||
|
default: json.freeform.default
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,19 @@ export interface TagRenderingConfigJson {
|
||||||
**/
|
**/
|
||||||
addExtraTags?: string[];
|
addExtraTags?: string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When set, influences the way a question is asked.
|
||||||
|
* Instead of showing a full-widht text field, the text field will be shown within the rendering of the question.
|
||||||
|
*
|
||||||
|
* This combines badly with special input elements, as it'll distort the layout.
|
||||||
|
*/
|
||||||
|
inline?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default value to enter if no previous tagging is present.
|
||||||
|
* Normally undefined (aka do not enter anything)
|
||||||
|
*/
|
||||||
|
default?: string
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
35
UI/Input/InputElementWrapper.ts
Normal file
35
UI/Input/InputElementWrapper.ts
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import {InputElement} from "./InputElement";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import BaseUIElement from "../BaseUIElement";
|
||||||
|
import {Translation} from "../i18n/Translation";
|
||||||
|
import {SubstitutedTranslation} from "../SubstitutedTranslation";
|
||||||
|
|
||||||
|
export default class InputElementWrapper<T> extends InputElement<T> {
|
||||||
|
public readonly IsSelected: UIEventSource<boolean>;
|
||||||
|
private readonly _inputElement: InputElement<T>;
|
||||||
|
private readonly _renderElement: BaseUIElement
|
||||||
|
|
||||||
|
constructor(inputElement: InputElement<T>, translation: Translation, key: string, tags: UIEventSource<any>) {
|
||||||
|
super()
|
||||||
|
this._inputElement = inputElement;
|
||||||
|
this.IsSelected = inputElement.IsSelected
|
||||||
|
const mapping = new Map<string, BaseUIElement>()
|
||||||
|
|
||||||
|
mapping.set(key, inputElement)
|
||||||
|
|
||||||
|
this._renderElement = new SubstitutedTranslation(translation, tags, mapping)
|
||||||
|
}
|
||||||
|
|
||||||
|
GetValue(): UIEventSource<T> {
|
||||||
|
return this._inputElement.GetValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
IsValid(t: T): boolean {
|
||||||
|
return this._inputElement.IsValid(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected InnerConstructElement(): HTMLElement {
|
||||||
|
return this._renderElement.ConstructElement();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -36,11 +36,11 @@ export class TextField extends InputElement<string> {
|
||||||
this.SetClass("form-text-field")
|
this.SetClass("form-text-field")
|
||||||
let inputEl: HTMLElement
|
let inputEl: HTMLElement
|
||||||
if (options.htmlType === "area") {
|
if (options.htmlType === "area") {
|
||||||
|
this.SetClass("w-full box-border max-w-full")
|
||||||
const el = document.createElement("textarea")
|
const el = document.createElement("textarea")
|
||||||
el.placeholder = placeholder
|
el.placeholder = placeholder
|
||||||
el.rows = options.textAreaRows
|
el.rows = options.textAreaRows
|
||||||
el.cols = 50
|
el.cols = 50
|
||||||
el.style.cssText = "max-width: 100%; width: 100%; box-sizing: border-box"
|
|
||||||
inputEl = el;
|
inputEl = el;
|
||||||
} else {
|
} else {
|
||||||
const el = document.createElement("input")
|
const el = document.createElement("input")
|
||||||
|
|
|
@ -282,7 +282,7 @@ export default class ValidatedTextField {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
unitDropDown.GetValue().setData(unit.defaultDenom)
|
unitDropDown.GetValue().setData(unit.defaultDenom)
|
||||||
unitDropDown.SetStyle("width: min-content")
|
unitDropDown.SetClass("w-min")
|
||||||
|
|
||||||
input = new CombinedInputElement(
|
input = new CombinedInputElement(
|
||||||
input,
|
input,
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {TagUtils} from "../../Logic/Tags/TagUtils";
|
||||||
import BaseUIElement from "../BaseUIElement";
|
import BaseUIElement from "../BaseUIElement";
|
||||||
import {DropDown} from "../Input/DropDown";
|
import {DropDown} from "../Input/DropDown";
|
||||||
import {Unit} from "../../Customizations/JSON/Denomination";
|
import {Unit} from "../../Customizations/JSON/Denomination";
|
||||||
|
import InputElementWrapper from "../Input/InputElementWrapper";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the question element.
|
* Shows the question element.
|
||||||
|
@ -128,7 +129,7 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
}
|
}
|
||||||
return Utils.NoNull(configuration.mappings?.map((m,i) => excludeIndex === i ? undefined: m.ifnot))
|
return Utils.NoNull(configuration.mappings?.map((m,i) => excludeIndex === i ? undefined: m.ifnot))
|
||||||
}
|
}
|
||||||
const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource.data);
|
const ff = TagRenderingQuestion.GenerateFreeform(configuration, applicableUnit, tagsSource);
|
||||||
const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0
|
const hasImages = mappings.filter(mapping => mapping.then.ExtractImages().length > 0).length > 0
|
||||||
|
|
||||||
if (mappings.length < 8 || configuration.multiAnswer || hasImages) {
|
if (mappings.length < 8 || configuration.multiAnswer || hasImages) {
|
||||||
|
@ -289,7 +290,7 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
(t0, t1) => t1.isEquivalent(t0));
|
(t0, t1) => t1.isEquivalent(t0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tagsData: any): InputElement<TagsFilter> {
|
private static GenerateFreeform(configuration: TagRenderingConfig, applicableUnit: Unit, tags: UIEventSource<any>): InputElement<TagsFilter> {
|
||||||
const freeform = configuration.freeform;
|
const freeform = configuration.freeform;
|
||||||
if (freeform === undefined) {
|
if (freeform === undefined) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -328,7 +329,8 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, {
|
const tagsData = tags.data;
|
||||||
|
const input: InputElement<string> = ValidatedTextField.InputForType(configuration.freeform.type, {
|
||||||
isValid: (str) => (str.length <= 255),
|
isValid: (str) => (str.length <= 255),
|
||||||
country: () => tagsData._country,
|
country: () => tagsData._country,
|
||||||
location: [tagsData._lat, tagsData._lon],
|
location: [tagsData._lat, tagsData._lon],
|
||||||
|
@ -336,12 +338,22 @@ export default class TagRenderingQuestion extends Combine {
|
||||||
unit: applicableUnit
|
unit: applicableUnit
|
||||||
});
|
});
|
||||||
|
|
||||||
input.GetValue().setData(tagsData[configuration.freeform.key]);
|
input.GetValue().setData(tagsData[freeform.key] ?? freeform.default);
|
||||||
|
|
||||||
return new InputElementMap(
|
let inputTagsFilter : InputElement<TagsFilter> = new InputElementMap(
|
||||||
input, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
|
input, (a, b) => a === b || (a?.isEquivalent(b) ?? false),
|
||||||
pickString, toString
|
pickString, toString
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if(freeform.inline){
|
||||||
|
|
||||||
|
inputTagsFilter.SetClass("w-16-imp")
|
||||||
|
inputTagsFilter = new InputElementWrapper(inputTagsFilter, configuration.render, freeform.key, tags)
|
||||||
|
inputTagsFilter.SetClass("block")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputTagsFilter;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -379,6 +379,7 @@ export default class SpecialVisualizations {
|
||||||
}
|
}
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
|
static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage();
|
||||||
private static GenHelpMessage() {
|
private static GenHelpMessage() {
|
||||||
|
|
||||||
|
|
|
@ -7,19 +7,43 @@ import SpecialVisualizations, {SpecialVisualization} from "./SpecialVisualizatio
|
||||||
import {Utils} from "../Utils";
|
import {Utils} from "../Utils";
|
||||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
import {VariableUiElement} from "./Base/VariableUIElement";
|
||||||
import Combine from "./Base/Combine";
|
import Combine from "./Base/Combine";
|
||||||
|
import BaseUIElement from "./BaseUIElement";
|
||||||
|
|
||||||
export class SubstitutedTranslation extends VariableUiElement {
|
export class SubstitutedTranslation extends VariableUiElement {
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
translation: Translation,
|
translation: Translation,
|
||||||
tagsSource: UIEventSource<any>) {
|
tagsSource: UIEventSource<any>,
|
||||||
|
mapping: Map<string, BaseUIElement> = undefined) {
|
||||||
|
|
||||||
|
const extraMappings: SpecialVisualization[] = [];
|
||||||
|
|
||||||
|
mapping?.forEach((value, key) => {
|
||||||
|
console.log("KV:", key, value)
|
||||||
|
extraMappings.push(
|
||||||
|
{
|
||||||
|
funcName: key,
|
||||||
|
constr: (() => {
|
||||||
|
return value
|
||||||
|
}),
|
||||||
|
docs: "Dynamically injected input element",
|
||||||
|
args: [],
|
||||||
|
example: ""
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
super(
|
super(
|
||||||
Locale.language.map(language => {
|
Locale.language.map(language => {
|
||||||
const txt = translation.textFor(language)
|
let txt = translation.textFor(language);
|
||||||
if (txt === undefined) {
|
if (txt === undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt).map(
|
mapping?.forEach((_, key) => {
|
||||||
|
txt = txt.replace(new RegExp(`{${key}}`, "g"), `{${key}()}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
return new Combine(SubstitutedTranslation.ExtractSpecialComponents(txt, extraMappings).map(
|
||||||
proto => {
|
proto => {
|
||||||
if (proto.fixed !== undefined) {
|
if (proto.fixed !== undefined) {
|
||||||
return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags)));
|
return new VariableUiElement(tagsSource.map(tags => Utils.SubstituteKeys(proto.fixed, tags)));
|
||||||
|
@ -36,30 +60,35 @@ export class SubstitutedTranslation extends VariableUiElement {
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
this.SetClass("w-full")
|
this.SetClass("w-full")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static ExtractSpecialComponents(template: string): {
|
public static ExtractSpecialComponents(template: string, extraMappings: SpecialVisualization[] = []): {
|
||||||
fixed?: string, special?: {
|
fixed?: string,
|
||||||
|
special?: {
|
||||||
func: SpecialVisualization,
|
func: SpecialVisualization,
|
||||||
args: string[],
|
args: string[],
|
||||||
style: string
|
style: string
|
||||||
}
|
}
|
||||||
}[] {
|
}[] {
|
||||||
|
|
||||||
for (const knownSpecial of SpecialVisualizations.specialVisualizations) {
|
if (extraMappings.length > 0) {
|
||||||
|
|
||||||
|
console.log("Extra mappings are", extraMappings)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) {
|
||||||
|
|
||||||
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
// Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
|
||||||
const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`);
|
const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)(:.*)?}(.*)`);
|
||||||
if (matched != null) {
|
if (matched != null) {
|
||||||
|
|
||||||
// We found a special component that should be brought to live
|
// We found a special component that should be brought to live
|
||||||
const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1]);
|
const partBefore = SubstitutedTranslation.ExtractSpecialComponents(matched[1], extraMappings);
|
||||||
const argument = matched[2].trim();
|
const argument = matched[2].trim();
|
||||||
const style = matched[3]?.substring(1) ?? ""
|
const style = matched[3]?.substring(1) ?? ""
|
||||||
const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4]);
|
const partAfter = SubstitutedTranslation.ExtractSpecialComponents(matched[4], extraMappings);
|
||||||
const args = knownSpecial.args.map(arg => arg.defaultValue ?? "");
|
const args = knownSpecial.args.map(arg => arg.defaultValue ?? "");
|
||||||
if (argument.length > 0) {
|
if (argument.length > 0) {
|
||||||
const realArgs = argument.split(",").map(str => str.trim());
|
const realArgs = argument.split(",").map(str => str.trim());
|
||||||
|
@ -73,11 +102,13 @@ export class SubstitutedTranslation extends VariableUiElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
let element;
|
let element;
|
||||||
element = {special:{
|
element = {
|
||||||
args: args,
|
special: {
|
||||||
style: style,
|
args: args,
|
||||||
func: knownSpecial
|
style: style,
|
||||||
}}
|
func: knownSpecial
|
||||||
|
}
|
||||||
|
}
|
||||||
return [...partBefore, element, ...partAfter]
|
return [...partBefore, element, ...partAfter]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,8 @@
|
||||||
},
|
},
|
||||||
"freeform": {
|
"freeform": {
|
||||||
"key": "capacity",
|
"key": "capacity",
|
||||||
"type": "nat"
|
"type": "nat",
|
||||||
|
"inline": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -105,6 +105,10 @@ a {
|
||||||
width: min-content;
|
width: min-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-16-imp {
|
||||||
|
width: 4rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
.space-between{
|
.space-between{
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue